euler314's picture
Update app.py
e509f96 verified
raw
history blame
59.5 kB
import gradio as gr
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import tropycal.tracks as tracks
import pickle
import requests
import os
import argparse
from datetime import datetime
import statsmodels.api as sm
import shutil
import tempfile
import csv
from collections import defaultdict
import filecmp
from sklearn.manifold import TSNE
from sklearn.cluster import DBSCAN
from scipy.interpolate import interp1d
# Command-line argument parsing
parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
args = parser.parse_args()
DATA_PATH = args.data_path
ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
# Basin-specific files instead of the global file
BASIN_FILES = {
'EP': 'ibtracs.EP.list.v04r01.csv',
'NA': 'ibtracs.NA.list.v04r01.csv',
'WP': 'ibtracs.WP.list.v04r01.csv'
}
IBTRACS_BASE_URL = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/'
LOCAL_MERGED_PATH = os.path.join(DATA_PATH, 'ibtracs.merged.v04r01.csv')
CACHE_FILE = 'ibtracs_cache.pkl'
CACHE_EXPIRY_DAYS = 0 # Force refresh for testing
# Color maps for Plotly (RGB)
color_map = {
'C5 Super Typhoon': 'rgb(255, 0, 0)',
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
'C3 Strong Typhoon': 'rgb(255, 255, 0)',
'C2 Typhoon': 'rgb(0, 255, 0)',
'C1 Typhoon': 'rgb(0, 255, 255)',
'Tropical Storm': 'rgb(0, 0, 255)',
'Tropical Depression': 'rgb(128, 128, 128)'
}
# Classification standards with distinct colors for Matplotlib
atlantic_standard = {
'C5 Super Typhoon': {'wind_speed': 137, 'color': 'Red', 'hex': '#FF0000'},
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'Orange', 'hex': '#FFA500'},
'C3 Strong Typhoon': {'wind_speed': 96, 'color': 'Yellow', 'hex': '#FFFF00'},
'C2 Typhoon': {'wind_speed': 83, 'color': 'Green', 'hex': '#00FF00'},
'C1 Typhoon': {'wind_speed': 64, 'color': 'Cyan', 'hex': '#00FFFF'},
'Tropical Storm': {'wind_speed': 34, 'color': 'Blue', 'hex': '#0000FF'},
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
}
taiwan_standard = {
'Strong Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'},
'Medium Typhoon': {'wind_speed': 33.7, 'color': 'Orange', 'hex': '#FFA500'},
'Mild Typhoon': {'wind_speed': 17.2, 'color': 'Yellow', 'hex': '#FFFF00'},
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
}
# Season months mapping
season_months = {
'all': list(range(1, 13)),
'summer': [6, 7, 8],
'winter': [12, 1, 2]
}
# Regions for duration calculations
regions = {
"Taiwan Land": {"lat_min": 21.8, "lat_max": 25.3, "lon_min": 119.5, "lon_max": 122.1},
"Taiwan Sea": {"lat_min": 19, "lat_max": 28, "lon_min": 117, "lon_max": 125},
"Japan": {"lat_min": 20, "lat_max": 45, "lon_min": 120, "lon_max": 150},
"China": {"lat_min": 18, "lat_max": 53, "lon_min": 73, "lon_max": 135},
"Hong Kong": {"lat_min": 21.5, "lat_max": 23, "lon_min": 113, "lon_max": 115},
"Philippines": {"lat_min": 5, "lat_max": 21, "lon_min": 115, "lon_max": 130}
}
# Data loading and preprocessing functions
def download_oni_file(url, filename):
response = requests.get(url)
response.raise_for_status()
with open(filename, 'wb') as f:
f.write(response.content)
return True
def convert_oni_ascii_to_csv(input_file, output_file):
data = defaultdict(lambda: [''] * 12)
season_to_month = {'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11}
with open(input_file, 'r') as f:
lines = f.readlines()[1:]
for line in lines:
parts = line.split()
if len(parts) >= 4:
season, year, anom = parts[0], parts[1], parts[-1]
if season in season_to_month:
month = season_to_month[season]
if season == 'DJF':
year = str(int(year) - 1)
data[year][month-1] = anom
with open(output_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
for year in sorted(data.keys()):
writer.writerow([year] + data[year])
def update_oni_data():
url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
input_file = os.path.join(DATA_PATH, "oni.ascii.txt")
output_file = ONI_DATA_PATH
if download_oni_file(url, temp_file):
if not os.path.exists(input_file) or not filecmp.cmp(temp_file, input_file):
os.replace(temp_file, input_file)
convert_oni_ascii_to_csv(input_file, output_file)
else:
os.remove(temp_file)
def load_ibtracs_data():
if os.path.exists(CACHE_FILE) and (datetime.now() - datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))).days < CACHE_EXPIRY_DAYS:
with open(CACHE_FILE, 'rb') as f:
return pickle.load(f)
try:
# Check if merged file already exists
if os.path.exists(LOCAL_MERGED_PATH):
print("Loading merged basins file...")
ibtracs = tracks.TrackDataset(source='ibtracs', ibtracs_url=LOCAL_MERGED_PATH)
else:
print("Downloading and merging basin files...")
# Create temporary file for merged data
header = None
with open(LOCAL_MERGED_PATH, 'w', newline='') as merged_file:
writer = None
# Download and process each basin file
for basin, filename in BASIN_FILES.items():
basin_url = IBTRACS_BASE_URL + filename
local_path = os.path.join(DATA_PATH, filename)
# Download the basin file if it doesn't exist
if not os.path.exists(local_path):
print(f"Downloading {basin} basin file...")
response = requests.get(basin_url)
response.raise_for_status()
with open(local_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded {basin} basin file.")
# Process and merge the basin file
with open(local_path, 'r', newline='') as basin_file:
reader = csv.reader(basin_file)
# Save header from the first file
if header is None:
header = next(reader)
writer = csv.writer(merged_file)
writer.writerow(header)
# Skip the second header line
next(reader)
else:
# Skip header lines in subsequent files
next(reader)
next(reader)
# Write all data rows
writer.writerows(reader)
print(f"Created merged basin file at {LOCAL_MERGED_PATH}")
ibtracs = tracks.TrackDataset(source='ibtracs', ibtracs_url=LOCAL_MERGED_PATH)
with open(CACHE_FILE, 'wb') as f:
pickle.dump(ibtracs, f)
return ibtracs
except Exception as e:
print(f"Error loading IBTrACS data: {e}")
print("Attempting to load default dataset...")
ibtracs = tracks.TrackDataset(basin='all')
with open(CACHE_FILE, 'wb') as f:
pickle.dump(ibtracs, f)
return ibtracs
def convert_typhoondata(input_file, output_file):
with open(input_file, 'r') as infile:
next(infile); next(infile)
reader = csv.reader(infile)
sid_data = defaultdict(list)
for row in reader:
if row:
sid = row[0]
sid_data[sid].append((row, row[6]))
with open(output_file, 'w', newline='') as outfile:
fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME', 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES', 'START_DATE', 'END_DATE']
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
writer.writeheader()
for sid, data in sid_data.items():
start_date = min(data, key=lambda x: x[1])[1]
end_date = max(data, key=lambda x: x[1])[1]
for row, iso_time in data:
writer.writerow({
'SID': row[0], 'ISO_TIME': iso_time, 'LAT': row[8], 'LON': row[9], 'SEASON': row[1], 'NAME': row[5],
'WMO_WIND': row[10].strip() or ' ', 'WMO_PRES': row[11].strip() or ' ',
'USA_WIND': row[23].strip() or ' ', 'USA_PRES': row[24].strip() or ' ',
'START_DATE': start_date, 'END_DATE': end_date
})
def load_data(oni_path, typhoon_path):
oni_data = pd.read_csv(oni_path)
typhoon_data = pd.read_csv(typhoon_path, low_memory=False)
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
typhoon_data = typhoon_data.dropna(subset=['ISO_TIME'])
return oni_data, typhoon_data
def process_oni_data(oni_data):
oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
month_map = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'}
oni_long['Month'] = oni_long['Month'].map(month_map)
oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str) + '-' + oni_long['Month'] + '-01')
oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
return oni_long
def process_typhoon_data(typhoon_data):
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
# Debug: Check unique basins
unique_basins = typhoon_data['SID'].str[:2].unique()
print(f"Unique basins in typhoon_data: {unique_basins}")
typhoon_max = typhoon_data.groupby('SID').agg({
'USA_WIND': 'max', 'USA_PRES': 'min', 'ISO_TIME': 'first', 'SEASON': 'first', 'NAME': 'first',
'LAT': 'first', 'LON': 'first'
}).reset_index()
typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon)
return typhoon_max
def merge_data(oni_long, typhoon_max):
return pd.merge(typhoon_max, oni_long, on=['Year', 'Month'])
def categorize_typhoon(wind_speed):
wind_speed_kt = wind_speed
if wind_speed_kt >= 137:
return 'C5 Super Typhoon'
elif wind_speed_kt >= 113:
return 'C4 Very Strong Typhoon'
elif wind_speed_kt >= 96:
return 'C3 Strong Typhoon'
elif wind_speed_kt >= 83:
return 'C2 Typhoon'
elif wind_speed_kt >= 64:
return 'C1 Typhoon'
elif wind_speed_kt >= 34:
return 'Tropical Storm'
else:
return 'Tropical Depression'
def classify_enso_phases(oni_value):
if isinstance(oni_value, pd.Series):
oni_value = oni_value.iloc[0]
if oni_value >= 0.5:
return 'El Nino'
elif oni_value <= -0.5:
return 'La Nina'
else:
return 'Neutral'
# Load data globally
update_oni_data()
ibtracs = load_ibtracs_data()
convert_typhoondata(LOCAL_MERGED_PATH, TYPHOON_DATA_PATH)
oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
oni_long = process_oni_data(oni_data)
typhoon_max = process_typhoon_data(typhoon_data)
merged_data = merge_data(oni_long, typhoon_max)
# Main analysis functions (using Plotly)
def generate_typhoon_tracks(filtered_data, typhoon_search):
fig = go.Figure()
for sid in filtered_data['SID'].unique():
storm_data = filtered_data[filtered_data['SID'] == sid]
color = {'El Nino': 'red', 'La Nina': 'blue', 'Neutral': 'green'}[storm_data['ENSO_Phase'].iloc[0]]
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
name=storm_data['NAME'].iloc[0], line=dict(width=2, color=color)
))
if typhoon_search:
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if mask.any():
storm_data = filtered_data[mask]
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
name=f'Matched: {typhoon_search}', line=dict(width=5, color='yellow')
))
fig.update_layout(
title='Typhoon Tracks',
geo=dict(projection_type='natural earth', showland=True),
height=700
)
return fig
def generate_wind_oni_scatter(filtered_data, typhoon_search):
fig = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category', hover_data=['NAME', 'Year', 'Category'],
title='Wind Speed vs ONI', labels={'ONI': 'ONI Value', 'USA_WIND': 'Max Wind Speed (knots)'},
color_discrete_map=color_map)
if typhoon_search:
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if mask.any():
fig.add_trace(go.Scatter(
x=filtered_data.loc[mask, 'ONI'], y=filtered_data.loc[mask, 'USA_WIND'],
mode='markers', marker=dict(size=10, color='red', symbol='star'),
name=f'Matched: {typhoon_search}',
text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')'
))
return fig
def generate_pressure_oni_scatter(filtered_data, typhoon_search):
fig = px.scatter(filtered_data, x='ONI', y='USA_PRES', color='Category', hover_data=['NAME', 'Year', 'Category'],
title='Pressure vs ONI', labels={'ONI': 'ONI Value', 'USA_PRES': 'Min Pressure (hPa)'},
color_discrete_map=color_map)
if typhoon_search:
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if mask.any():
fig.add_trace(go.Scatter(
x=filtered_data.loc[mask, 'ONI'], y=filtered_data.loc[mask, 'USA_PRES'],
mode='markers', marker=dict(size=10, color='red', symbol='star'),
name=f'Matched: {typhoon_search}',
text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')'
))
return fig
def generate_regression_analysis(filtered_data):
fig = px.scatter(filtered_data, x='LON', y='ONI', hover_data=['NAME'],
title='Typhoon Generation Longitude vs ONI (All Years)')
if len(filtered_data) > 1:
X = np.array(filtered_data['LON']).reshape(-1, 1)
y = filtered_data['ONI']
model = sm.OLS(y, sm.add_constant(X)).fit()
y_pred = model.predict(sm.add_constant(X))
fig.add_trace(go.Scatter(x=filtered_data['LON'], y=y_pred, mode='lines', name='Regression Line'))
slope = model.params[1]
slopes_text = f"All Years Slope: {slope:.4f}"
else:
slopes_text = "Insufficient data for regression"
return fig, slopes_text
def generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
filtered_data = merged_data[
(merged_data['ISO_TIME'] >= start_date) &
(merged_data['ISO_TIME'] <= end_date)
]
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
if enso_phase != 'all':
filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
tracks_fig = generate_typhoon_tracks(filtered_data, typhoon_search)
wind_scatter = generate_wind_oni_scatter(filtered_data, typhoon_search)
pressure_scatter = generate_pressure_oni_scatter(filtered_data, typhoon_search)
regression_fig, slopes_text = generate_regression_analysis(filtered_data)
return tracks_fig, wind_scatter, pressure_scatter, regression_fig, slopes_text
# Get full tracks function for Track Visualization tab
def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
filtered_data = merged_data[
(merged_data['ISO_TIME'] >= start_date) &
(merged_data['ISO_TIME'] <= end_date)
]
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
if enso_phase != 'all':
filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
unique_storms = filtered_data['SID'].unique()
count = len(unique_storms)
fig = go.Figure()
for sid in unique_storms:
storm_data = typhoon_data[typhoon_data['SID'] == sid]
name = storm_data['NAME'].iloc[0] if not pd.isna(storm_data['NAME'].iloc[0]) else "Unnamed"
storm_oni = filtered_data[filtered_data['SID'] == sid]['ONI'].iloc[0]
color = 'red' if storm_oni >= 0.5 else ('blue' if storm_oni <= -0.5 else 'green')
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
name=f"{name} ({storm_data['SEASON'].iloc[0]})",
line=dict(width=1.5, color=color),
hoverinfo="name"
))
if typhoon_search:
search_mask = typhoon_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if search_mask.any():
for sid in typhoon_data[search_mask]['SID'].unique():
storm_data = typhoon_data[typhoon_data['SID'] == sid]
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines+markers',
name=f"MATCHED: {storm_data['NAME'].iloc[0]} ({storm_data['SEASON'].iloc[0]})",
line=dict(width=3, color='yellow'),
marker=dict(size=5),
hoverinfo="name"
))
fig.update_layout(
title=f"Typhoon Tracks ({start_year}-{start_month} to {end_year}-{end_month})",
geo=dict(
projection_type='natural earth',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(204, 204, 204)',
center=dict(lon=140, lat=20),
projection_scale=3
),
legend_title="Typhoons by ENSO Phase",
showlegend=True,
height=700
)
fig.add_annotation(
x=0.02, y=0.98, xref="paper", yref="paper",
text="Red: El Niño, Blue: La Niña, Green: Neutral",
showarrow=False, align="left",
bgcolor="rgba(255,255,255,0.8)"
)
return fig, f"Total typhoons displayed: {count}"
# Analysis functions for Wind, Pressure, and Longitude tabs
def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
results = generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search)
regression = perform_wind_regression(start_year, start_month, end_year, end_month)
return results[1], regression
def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
results = generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search)
regression = perform_pressure_regression(start_year, start_month, end_year, end_month)
return results[2], regression
def get_longitude_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
results = generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search)
regression = perform_longitude_regression(start_year, start_month, end_year, end_month)
return results[3], results[4], regression
# Video animation function with fixed sidebar and wind radius visualization
def categorize_typhoon_by_standard(wind_speed, standard):
if standard == 'taiwan':
wind_speed_ms = wind_speed * 0.514444
if wind_speed_ms >= 51.0:
return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['hex']
elif wind_speed_ms >= 33.7:
return 'Medium Typhoon', taiwan_standard['Medium Typhoon']['hex']
elif wind_speed_ms >= 17.2:
return 'Mild Typhoon', taiwan_standard['Mild Typhoon']['hex']
return 'Tropical Depression', taiwan_standard['Tropical Depression']['hex']
else:
if wind_speed >= 137:
return 'C5 Super Typhoon', atlantic_standard['C5 Super Typhoon']['hex']
elif wind_speed >= 113:
return 'C4 Very Strong Typhoon', atlantic_standard['C4 Very Strong Typhoon']['hex']
elif wind_speed >= 96:
return 'C3 Strong Typhoon', atlantic_standard['C3 Strong Typhoon']['hex']
elif wind_speed >= 83:
return 'C2 Typhoon', atlantic_standard['C2 Typhoon']['hex']
elif wind_speed >= 64:
return 'C1 Typhoon', atlantic_standard['C1 Typhoon']['hex']
elif wind_speed >= 34:
return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
def generate_track_video(year, basin, typhoon, standard):
if not typhoon:
return None
typhoon_id = typhoon.split('(')[-1].strip(')')
storm = ibtracs.get_storm(typhoon_id)
# Map focus
min_lat, max_lat = min(storm.lat), max(storm.lat)
min_lon, max_lon = min(storm.lon), max(storm.lon)
lat_padding = max((max_lat - min_lat) * 0.3, 5)
lon_padding = max((max_lon - min_lon) * 0.3, 5)
# Set up the figure (900x700 pixels at 100 DPI)
fig = plt.figure(figsize=(9, 7), dpi=100)
ax = plt.axes([0.05, 0.05, 0.65, 0.90], projection=ccrs.PlateCarree())
ax.set_extent([min_lon - lon_padding, max_lon + lon_padding, min_lat - lat_padding, max_lat + lat_padding], crs=ccrs.PlateCarree())
# Add basin name to the title
basin_name = basin
if basin == "WP":
basin_name = "Western North Pacific"
elif basin == "EP":
basin_name = "Eastern North Pacific"
elif basin == "NA" or basin == "AL":
basin_name = "North Atlantic"
elif basin == "NI" or basin == "IO":
basin_name = "North Indian"
elif basin == "SI":
basin_name = "South Indian"
elif basin == "SP":
basin_name = "South Pacific"
elif basin == "SA" or basin == "SL":
basin_name = "South Atlantic"
elif basin == "All":
basin_name = "Global Oceans"
# Add world map features
ax.add_feature(cfeature.LAND, facecolor='lightgray')
ax.add_feature(cfeature.OCEAN, facecolor='lightblue')
ax.add_feature(cfeature.COASTLINE, edgecolor='black')
ax.add_feature(cfeature.BORDERS, linestyle=':', edgecolor='gray')
ax.gridlines(draw_labels=True, linestyle='--', color='gray', alpha=0.5)
ax.set_title(f"{year} {storm.name} - {basin_name}")
# Initialize the line and point
line, = ax.plot([], [], 'b-', linewidth=2, transform=ccrs.PlateCarree())
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
# Initialize wind radius circles for 34kt, 50kt, and 64kt
radius_patches = []
for _ in range(3):
patch = plt.Circle((0, 0), 0, fill=False, linewidth=2, visible=False, transform=ccrs.PlateCarree())
radius_patches.append(ax.add_patch(patch))
# Add sidebar on the right with adjusted positions
details_title = fig.text(0.7, 0.95, "Cyclone Details", fontsize=12, fontweight='bold', verticalalignment='top')
details_text = fig.text(0.7, 0.85, '', fontsize=12, verticalalignment='top',
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
# Add color legend
standard_dict = atlantic_standard if standard == 'atlantic' else taiwan_standard
legend_elements = [plt.Line2D([0], [0], marker='o', color='w', label=f"{cat}",
markerfacecolor=details['hex'], markersize=10)
for cat, details in standard_dict.items()]
fig.legend(handles=legend_elements, title="Color Legend", loc='center right',
bbox_to_anchor=(0.95, 0.5), fontsize=10)
# Add wind radius legend
radius_legend = [
plt.Line2D([0], [0], color='blue', label='34kt Gale Force'),
plt.Line2D([0], [0], color='orange', label='50kt Storm Force'),
plt.Line2D([0], [0], color='red', label='64kt Hurricane Force')
]
fig.legend(handles=radius_legend, title="Wind Radii", loc='lower right',
bbox_to_anchor=(0.95, 0.15), fontsize=9)
def init():
line.set_data([], [])
point.set_data([], [])
date_text.set_text('')
details_text.set_text('')
for patch in radius_patches:
patch.set_center((0, 0))
patch.set_radius(0)
patch.set_visible(False)
return [line, point, date_text, details_text] + radius_patches
def update(frame):
line.set_data(storm.lon[:frame+1], storm.lat[:frame+1])
category, color = categorize_typhoon_by_standard(storm.vmax[frame], standard)
point.set_data([storm.lon[frame]], [storm.lat[frame]])
point.set_color(color)
date_text.set_text(storm.time[frame].strftime('%Y-%m-%d %H:%M'))
# Update wind radius circles
radius_info = []
# Check for radius data from different agencies
wind_thresholds = [(34, 'blue'), (50, 'orange'), (64, 'red')]
for i, (wind_kt, circle_color) in enumerate(wind_thresholds):
# Check USA agency radius data (average of all quadrants)
radius_values = []
# Check USA agency data
for quadrant in ['ne', 'se', 'sw', 'nw']:
attr = f'usa_r{wind_kt}_{quadrant}'
if hasattr(storm, attr) and frame < len(getattr(storm, attr)) and not np.isnan(getattr(storm, attr)[frame]):
radius_values.append(getattr(storm, attr)[frame])
# If no USA data, check BOM data
if not radius_values:
for quadrant in ['ne', 'se', 'sw', 'nw']:
attr = f'bom_r{wind_kt}_{quadrant}'
if hasattr(storm, attr) and frame < len(getattr(storm, attr)) and not np.isnan(getattr(storm, attr)[frame]):
radius_values.append(getattr(storm, attr)[frame])
# If still no data, try Reunion data
if not radius_values:
for quadrant in ['ne', 'se', 'sw', 'nw']:
attr = f'reunion_r{wind_kt}_{quadrant}'
if hasattr(storm, attr) and frame < len(getattr(storm, attr)) and not np.isnan(getattr(storm, attr)[frame]):
radius_values.append(getattr(storm, attr)[frame])
if radius_values:
# Calculate average radius (nautical miles)
avg_radius = np.mean(radius_values)
# Convert from nautical miles to approximate degrees (1 nm ≈ 1/60 degree)
radius_deg = avg_radius / 60.0
radius_patches[i].set_center((storm.lon[frame], storm.lat[frame]))
radius_patches[i].set_radius(radius_deg)
radius_patches[i].set_edgecolor(circle_color)
radius_patches[i].set_visible(True)
radius_info.append(f"{wind_kt}kt radius: {avg_radius:.1f} nm")
else:
radius_patches[i].set_visible(False)
radius_info.append(f"{wind_kt}kt radius: 0 nm")
# Add radius information to details
radius_text = "\n".join(radius_info)
# Get pressure value if available, otherwise show 'N/A'
pressure_value = storm.mslp[frame] if hasattr(storm, 'mslp') and frame < len(storm.mslp) and not np.isnan(storm.mslp[frame]) else 'N/A'
pressure_text = f"Pressure: {pressure_value if pressure_value != 'N/A' else 'N/A'} mb"
details = f"Name: {storm.name}\n" \
f"Date: {storm.time[frame].strftime('%Y-%m-%d %H:%M')}\n" \
f"Wind Speed: {storm.vmax[frame]:.1f} kt\n" \
f"{pressure_text}\n" \
f"Category: {category}\n" \
f"\nWind Radii:\n{radius_text}"
details_text.set_text(details)
return [line, point, date_text, details_text] + radius_patches
ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(storm.time),
interval=200, blit=True, repeat=True)
# Save as video
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
writer = animation.FFMpegWriter(fps=5, bitrate=1800)
ani.save(temp_file.name, writer=writer)
plt.close(fig)
return temp_file.name
def simplified_track_video(year, basin, typhoon, standard):
if not typhoon:
return None
# Extract storm ID from the dropdown selection
typhoon_id = typhoon.split('(')[-1].strip(')')
# Extract basin code from the basin selection
basin_code = "All"
if basin != "All Basins":
basin_code = basin.split(' - ')[0]
# Generate the animation
return generate_track_video(year, basin_code, typhoon, standard)
# Logistic regression functions
def perform_wind_regression(start_year, start_month, end_year, end_month):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['USA_WIND', 'ONI'])
data['severe_typhoon'] = (data['USA_WIND'] >= 64).astype(int)
X = sm.add_constant(data['ONI'])
y = data['severe_typhoon']
model = sm.Logit(y, X).fit()
beta_1, exp_beta_1, p_value = model.params['ONI'], np.exp(model.params['ONI']), model.pvalues['ONI']
return f"Wind Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
def perform_pressure_regression(start_year, start_month, end_year, end_month):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['USA_PRES', 'ONI'])
data['intense_typhoon'] = (data['USA_PRES'] <= 950).astype(int)
X = sm.add_constant(data['ONI'])
y = data['intense_typhoon']
model = sm.Logit(y, X).fit()
beta_1, exp_beta_1, p_value = model.params['ONI'], np.exp(model.params['ONI']), model.pvalues['ONI']
return f"Pressure Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
def perform_longitude_regression(start_year, start_month, end_year, end_month):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['LON', 'ONI'])
data['western_typhoon'] = (data['LON'] <= 140).astype(int)
X = sm.add_constant(data['ONI'])
y = data['western_typhoon']
model = sm.Logit(y, X).fit()
beta_1, exp_beta_1, p_value = model.params['ONI'], np.exp(model.params['ONI']), model.pvalues['ONI']
return f"Longitude Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
# t-SNE clustering functions
def filter_west_pacific_coordinates(lons, lats):
mask = (lons >= 100) & (lons <= 180) & (lats >= 0) & (lats <= 40)
return lons[mask], lats[mask]
def filter_storm_by_season(storm, season):
start_month = storm.time[0].month
if season == 'all':
return True
elif season == 'summer':
return 4 <= start_month <= 8
elif season == 'winter':
return 9 <= start_month <= 12
return False
def point_region(lat, lon):
twl = regions["Taiwan Land"]
if twl["lat_min"] <= lat <= twl["lat_max"] and twl["lon_min"] <= lon <= twl["lon_max"]:
return "Taiwan Land"
tws = regions["Taiwan Sea"]
if tws["lat_min"] <= lat <= tws["lat_max"] and tws["lon_min"] <= lon <= tws["lon_max"]:
if not (twl["lat_min"] <= lat <= twl["lat_max"] and twl["lon_min"] <= lon <= twl["lon_max"]):
return "Taiwan Sea"
for rg in ["Japan", "China", "Hong Kong", "Philippines"]:
box = regions[rg]
if box["lat_min"] <= lat <= box["lat_max"] and box["lon_min"] <= lon <= box["lon_max"]:
return rg
return None
def calculate_region_durations(lons, lats, times):
region_times = defaultdict(float)
point_regions_list = [point_region(lats[i], lons[i]) for i in range(len(lons))]
for i in range(len(lons) - 1):
dt = (times[i + 1] - times[i]).total_seconds() / 3600.0
r1 = point_regions_list[i]
r2 = point_regions_list[i + 1]
if r1 and r2:
if r1 == r2:
region_times[r1] += dt
else:
region_times[r1] += dt / 2
region_times[r2] += dt / 2
elif r1 and not r2:
region_times[r1] += dt / 2
elif r2 and not r1:
region_times[r2] += dt / 2
return dict(region_times)
def endpoint_region_label(cluster_label, cluster_labels, filtered_storms):
indices = np.where(cluster_labels == cluster_label)[0]
if len(indices) == 0:
return ""
end_count = defaultdict(int)
for idx in indices:
lons, lats, vmax_, mslp_, times = filtered_storms[idx]
reg = point_region(lats[-1], lons[-1])
if reg:
end_count[reg] += 1
if end_count:
max_reg = max(end_count, key=end_count.get)
ratio = end_count[max_reg] / len(indices)
if ratio > 0.5:
return max_reg
return ""
def dynamic_dbscan(tsne_results, min_clusters=10, max_clusters=20, eps_values=np.linspace(1.0, 10.0, 91)):
best_labels = None
best_n_clusters = 0
best_n_noise = len(tsne_results)
best_eps = None
for eps in eps_values:
dbscan = DBSCAN(eps=eps, min_samples=3)
labels = dbscan.fit_predict(tsne_results)
unique_labels = set(labels)
if -1 in unique_labels:
unique_labels.remove(-1)
n_clusters = len(unique_labels)
n_noise = np.sum(labels == -1)
if min_clusters <= n_clusters <= max_clusters and n_noise < best_n_noise:
best_labels = labels
best_n_clusters = n_clusters
best_n_noise = n_noise
best_eps = eps
if best_labels is None:
for eps in eps_values[::-1]:
dbscan = DBSCAN(eps=eps, min_samples=3)
labels = dbscan.fit_predict(tsne_results)
unique_labels = set(labels)
if -1 in unique_labels:
unique_labels.remove(-1)
n_clusters = len(unique_labels)
if n_clusters == max_clusters:
best_labels = labels
best_n_clusters = n_clusters
best_n_noise = np.sum(labels == -1)
best_eps = eps
break
return best_labels, best_n_clusters, best_n_noise, best_eps
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
start_date = datetime(int(start_year), int(start_month), 1)
end_date = datetime(int(end_year), int(end_month), 28)
all_storms_data = []
for year in range(int(start_year), int(end_year) + 1):
season_data = ibtracs.get_season(year)
for storm_id in season_data.summary()['id']:
storm = ibtracs.get_storm(storm_id)
if storm.time[0] >= start_date and storm.time[-1] <= end_date and filter_storm_by_season(storm, season):
lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
if len(lons) > 1:
start_time = storm.time[0]
start_year_storm = start_time.year
start_month_storm = start_time.month
oni_row = oni_long[(oni_long['Year'] == start_year_storm) & (oni_long['Month'] == f'{start_month_storm:02d}')]
if not oni_row.empty:
oni_value_storm = oni_row['ONI'].iloc[0]
enso_phase_storm = classify_enso_phases(oni_value_storm)
if enso_value == 'all' or enso_phase_storm == enso_value.capitalize():
all_storms_data.append((lons, lats, np.array(storm.vmax), np.array(storm.mslp), np.array(storm.time), storm.name, enso_phase_storm))
if not all_storms_data:
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No storms found in the selected period."
# Prepare route vectors for t-SNE
max_length = max(len(st[0]) for st in all_storms_data)
route_vectors = []
filtered_storms = []
storms_vmax_list = []
storms_mslp_list = []
for idx, (lons, lats, vmax, mslp, times, name, enso_phase) in enumerate(all_storms_data):
t = np.linspace(0, 1, len(lons))
t_new = np.linspace(0, 1, max_length)
try:
lon_i = interp1d(t, lons, kind='linear', fill_value='extrapolate')(t_new)
lat_i = interp1d(t, lats, kind='linear', fill_value='extrapolate')(t_new)
vmax_i = interp1d(t, vmax, kind='linear', fill_value='extrapolate')(t_new)
if not np.all(np.isnan(mslp)):
mslp_i = interp1d(t, mslp, kind='linear', fill_value='extrapolate')(t_new)
else:
mslp_i = np.full(max_length, np.nan)
except Exception as e:
continue
route_vector = np.column_stack((lon_i, lat_i)).flatten()
if np.isnan(route_vector).any():
continue
route_vectors.append(route_vector)
filtered_storms.append((lons, lats, vmax_i, mslp_i, times))
storms_vmax_list.append(vmax_i)
storms_mslp_list.append(mslp_i)
route_vectors = np.array(route_vectors)
if len(route_vectors) == 0:
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms after interpolation."
# Perform t-SNE
tsne = TSNE(n_components=2, random_state=42, verbose=1)
tsne_results = tsne.fit_transform(route_vectors)
# Dynamic DBSCAN clustering
best_labels, best_n_clusters, best_n_noise, best_eps = dynamic_dbscan(tsne_results)
# Calculate region durations and mean routes
unique_labels = sorted(set(best_labels) - {-1})
label_to_idx = {label: i for i, label in enumerate(unique_labels)}
cluster_region_durations = [defaultdict(float) for _ in range(len(unique_labels))]
cluster_mean_routes = []
cluster_mean_vmax = []
cluster_mean_mslp = []
for i, (lons, lats, vmax, mslp, times) in enumerate(filtered_storms):
c = best_labels[i]
if c == -1:
continue
durations = calculate_region_durations(lons, lats, times)
idx = label_to_idx[c]
for r, val in durations.items():
cluster_region_durations[idx][r] += val
for c in unique_labels:
indices = np.where(best_labels == c)[0]
if len(indices) == 0:
cluster_mean_routes.append(([], []))
cluster_mean_vmax.append([])
cluster_mean_mslp.append([])
continue
cluster_lons = []
cluster_lats = []
cluster_v = []
cluster_p = []
for idx in indices:
lons, lats, vmax_, mslp_, times = filtered_storms[idx]
t = np.linspace(0, 1, len(lons))
t_new = np.linspace(0, 1, max_length)
lon_i = interp1d(t, lons, kind='linear', fill_value='extrapolate')(t_new)
lat_i = interp1d(t, lats, kind='linear', fill_value='extrapolate')(t_new)
cluster_lons.append(lon_i)
cluster_lats.append(lat_i)
cluster_v.append(storms_vmax_list[idx])
if not np.all(np.isnan(storms_mslp_list[idx])):
cluster_p.append(storms_mslp_list[idx])
if cluster_lons and cluster_lats:
mean_lon = np.mean(cluster_lons, axis=0)
mean_lat = np.mean(cluster_lats, axis=0)
mean_v = np.mean(cluster_v, axis=0)
if cluster_p:
mean_p = np.nanmean(cluster_p, axis=0)
else:
mean_p = np.full(max_length, np.nan)
cluster_mean_routes.append((mean_lon, mean_lat))
cluster_mean_vmax.append(mean_v)
cluster_mean_mslp.append(mean_p)
else:
cluster_mean_routes.append(([], []))
cluster_mean_vmax.append([])
cluster_mean_mslp.append([])
# t-SNE Scatter Plot
fig_tsne = go.Figure()
cluster_colors = px.colors.qualitative.Safe
if len(cluster_colors) < len(unique_labels):
cluster_colors = px.colors.qualitative.Dark24
for i, c in enumerate(unique_labels):
indices = np.where(best_labels == c)[0]
end_reg = endpoint_region_label(c, best_labels, filtered_storms)
name = f"Cluster {i+1}" + (f" (towards {end_reg})" if end_reg else "")
fig_tsne.add_trace(go.Scatter(
x=tsne_results[indices, 0],
y=tsne_results[indices, 1],
mode='markers',
marker=dict(size=5, color=cluster_colors[i % len(cluster_colors)]),
name=name
))
noise_indices = np.where(best_labels == -1)[0]
if len(noise_indices) > 0:
fig_tsne.add_trace(go.Scatter(
x=tsne_results[noise_indices, 0],
y=tsne_results[noise_indices, 1],
mode='markers',
marker=dict(size=5, color='grey'),
name='Noise'
))
fig_tsne.update_layout(
title="TSNE of Typhoon Routes",
xaxis_title="TSNE Dim 1",
yaxis_title="TSNE Dim 2",
legend_title="Clusters"
)
# Typhoon Routes Plot with Mean Routes
fig_routes = go.Figure()
for i, (lons, lats, _, _, _) in enumerate(filtered_storms):
c = best_labels[i]
if c == -1:
continue
color_idx = label_to_idx[c]
fig_routes.add_trace(
go.Scattergeo(
lon=lons,
lat=lats,
mode='lines',
opacity=0.3,
line=dict(width=1, color=cluster_colors[color_idx % len(cluster_colors)]),
showlegend=False
)
)
for i, c in enumerate(unique_labels):
mean_lon, mean_lat = cluster_mean_routes[i]
if len(mean_lon) == 0:
continue
end_reg = endpoint_region_label(c, best_labels, filtered_storms)
name = f"Cluster {i+1}" + (f" (towards {end_reg})" if end_reg else "")
fig_routes.add_trace(
go.Scattergeo(
lon=mean_lon,
lat=mean_lat,
mode='lines',
line=dict(width=4, color=cluster_colors[i % len(cluster_colors)]),
name=name
)
)
fig_routes.add_trace(
go.Scattergeo(
lon=[mean_lon[0]],
lat=[mean_lat[0]],
mode='markers',
marker=dict(size=10, color='green', symbol='triangle-up'),
name=f"Cluster {i+1} Start"
)
)
fig_routes.add_trace(
go.Scattergeo(
lon=[mean_lon[-1]],
lat=[mean_lat[-1]],
mode='markers',
marker=dict(size=10, color='red', symbol='x'),
name=f"Cluster {i+1} End"
)
)
enso_phase_text = {'all': 'All Years', 'El Nino': 'El Niño', 'La Nina': 'La Niña', 'Neutral': 'Neutral Years'}
fig_routes.update_layout(
title=f"West Pacific Typhoon Routes ({start_year}-{end_year}, {season.capitalize()}, {enso_phase_text.get(enso_value, 'All Years')})",
geo=dict(scope='asia', projection_type='mercator', showland=True, landcolor='lightgray')
)
# Cluster Statistics Plot
fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True, subplot_titles=("Average Wind Speed", "Average Pressure"))
for i, c in enumerate(unique_labels):
if len(cluster_mean_vmax[i]) > 0:
end_reg = endpoint_region_label(c, best_labels, filtered_storms)
name = f"Cluster {i+1}" + (f" ({end_reg})" if end_reg else "")
fig_stats.add_trace(
go.Scatter(y=cluster_mean_vmax[i], mode='lines', line=dict(width=2, color=cluster_colors[i % len(cluster_colors)]), name=name),
row=1, col=1
)
if not np.all(np.isnan(cluster_mean_mslp[i])):
fig_stats.add_trace(
go.Scatter(y=cluster_mean_mslp[i], mode='lines', line=dict(width=2, color=cluster_colors[i % len(cluster_colors)]), name=name),
row=2, col=1
)
fig_stats.update_layout(
title="Cluster Average Wind & Pressure Profiles",
xaxis_title="Route Normalized Index",
yaxis_title="Wind Speed (knots)",
xaxis2_title="Route Normalized Index",
yaxis2_title="Pressure (hPa)",
showlegend=True,
legend_tracegroupgap=300
)
# Cluster Information
cluster_info_lines = [f"Selected DBSCAN eps: {best_eps:.2f}", f"Number of noise points: {best_n_noise}"]
for i, c in enumerate(unique_labels):
indices = np.where(best_labels == c)[0]
count = len(indices)
if count == 0:
continue
avg_durations = {r: (cluster_region_durations[i][r] / count) for r in cluster_region_durations[i]}
end_reg = endpoint_region_label(c, best_labels, filtered_storms)
name = f"Cluster {i+1}" + (f" (towards {end_reg})" if end_reg else "")
cluster_info_lines.append(f"\n{name}")
if avg_durations:
for reg, hrs in avg_durations.items():
cluster_info_lines.append(f"{reg}: {hrs:.2f} hours")
else:
cluster_info_lines.append("No significant region durations.")
if end_reg in ["Taiwan Land", "Taiwan Sea"] and len(cluster_mean_vmax[i]) > 0:
final_wind = cluster_mean_vmax[i][-1]
if final_wind >= 34:
cluster_info_lines.append(
"CWA would issue a land warning ~18 hours before arrival." if end_reg == "Taiwan Land"
else "CWA would issue a sea warning ~24 hours before arrival."
)
if len(noise_indices) > 0:
cluster_info_lines.append(f"\nNoise Cluster\nNumber of storms classified as noise: {len(noise_indices)}")
cluster_info_text = "\n".join(cluster_info_lines)
return fig_tsne, fig_routes, fig_stats, cluster_info_text
# Define the basin to prefix mapping
basin_to_prefix = {
"All Basins": None,
"NA - North Atlantic": "AL",
"EP - Eastern North Pacific": "EP",
"WP - Western North Pacific": "WP",
"NI - North Indian": ["IO", "BB", "AS"], # Multiple prefixes for North Indian
"SI - South Indian": "SI",
"SP - Southern Pacific": "SP",
"SA - South Atlantic": "SL"
}
# Update typhoon options function for animation tab
def update_typhoon_options(year, basin):
try:
season = ibtracs.get_season(int(year))
storm_summary = season.summary()
# Get the prefix for filtering
prefix = basin_to_prefix.get(basin)
# Get all storms for the year
options = []
for i in range(len(storm_summary)):
try:
name = storm_summary['name'][i] if not pd.isna(storm_summary['name'][i]) else "Unnamed"
storm_id = storm_summary['id'][i]
# Filter by basin if a specific basin is selected
if prefix is None or (isinstance(prefix, list) and any(storm_id.startswith(p) for p in prefix)) or (not isinstance(prefix, list) and storm_id.startswith(prefix)):
options.append(f"{name} ({storm_id})")
except Exception:
continue
return gr.update(choices=options, value=options[0] if options else None)
except Exception as e:
print(f"Error updating typhoon options: {e}")
return gr.update(choices=[], value=None)
# Gradio Interface
with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
gr.Markdown("# Typhoon Analysis Dashboard")
with gr.Tab("Overview"):
gr.Markdown("""
## Welcome to the Typhoon Analysis Dashboard
This dashboard allows you to analyze typhoon data in relation to ENSO phases.
### Features:
- **Track Visualization**: View typhoon tracks by time period and ENSO phase
- **Wind Analysis**: Examine wind speed vs ONI relationships
- **Pressure Analysis**: Analyze pressure vs ONI relationships
- **Longitude Analysis**: Study typhoon generation longitude vs ONI
- **Path Animation**: Watch animated tropical cyclone paths with wind radius visualization
- **TSNE Cluster**: Perform t-SNE clustering on typhoon routes with mean routes and region analysis
Select a tab above to begin your analysis.
""")
with gr.Tab("Track Visualization"):
with gr.Row():
start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
typhoon_search = gr.Textbox(label="Typhoon Search")
analyze_btn = gr.Button("Generate Tracks")
tracks_plot = gr.Plot(label="Typhoon Tracks", elem_id="tracks_plot")
typhoon_count = gr.Textbox(label="Number of Typhoons Displayed")
analyze_btn.click(
fn=get_full_tracks,
inputs=[start_year, start_month, end_year, end_month, enso_phase, typhoon_search],
outputs=[tracks_plot, typhoon_count]
)
with gr.Tab("Wind Analysis"):
with gr.Row():
wind_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
wind_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
wind_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
wind_typhoon_search = gr.Textbox(label="Typhoon Search")
wind_analyze_btn = gr.Button("Generate Wind Analysis")
wind_scatter = gr.Plot(label="Wind Speed vs ONI")
wind_regression_results = gr.Textbox(label="Wind Regression Results")
wind_analyze_btn.click(
fn=get_wind_analysis,
inputs=[wind_start_year, wind_start_month, wind_end_year, wind_end_month, wind_enso_phase, wind_typhoon_search],
outputs=[wind_scatter, wind_regression_results]
)
with gr.Tab("Pressure Analysis"):
with gr.Row():
pressure_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
pressure_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
pressure_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
pressure_typhoon_search = gr.Textbox(label="Typhoon Search")
pressure_analyze_btn = gr.Button("Generate Pressure Analysis")
pressure_scatter = gr.Plot(label="Pressure vs ONI")
pressure_regression_results = gr.Textbox(label="Pressure Regression Results")
pressure_analyze_btn.click(
fn=get_pressure_analysis,
inputs=[pressure_start_year, pressure_start_month, pressure_end_year, pressure_end_month, pressure_enso_phase, pressure_typhoon_search],
outputs=[pressure_scatter, pressure_regression_results]
)
with gr.Tab("Longitude Analysis"):
with gr.Row():
lon_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
lon_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
lon_analyze_btn = gr.Button("Generate Longitude Analysis")
regression_plot = gr.Plot(label="Longitude vs ONI")
slopes_text = gr.Textbox(label="Regression Slopes")
lon_regression_results = gr.Textbox(label="Longitude Regression Results")
lon_analyze_btn.click(
fn=get_longitude_analysis,
inputs=[lon_start_year, lon_start_month, lon_end_year, lon_end_month, lon_enso_phase, lon_typhoon_search],
outputs=[regression_plot, slopes_text, lon_regression_results]
)
with gr.Tab("Tropical Cyclone Path Animation"):
with gr.Row():
year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950, 2025)], value="2000")
basin_dropdown = gr.Dropdown(
label="Basin",
choices=[
"All Basins",
"NA - North Atlantic",
"EP - Eastern North Pacific",
"WP - Western North Pacific",
"NI - North Indian",
"SI - South Indian",
"SP - Southern Pacific",
"SA - South Atlantic"
],
value="WP - Western North Pacific"
)
with gr.Row():
typhoon_dropdown = gr.Dropdown(label="Tropical Cyclone")
standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
animate_btn = gr.Button("Generate Animation")
# Use format="mp4" to indicate we expect MP4 video, and no source parameter to avoid upload/webcam UI
path_video = gr.Video(label="Tropical Cyclone Path Animation", format="mp4", interactive=False, elem_id="path_video")
animation_info = gr.Markdown("""
### Animation Instructions
1. Select a year and basin from the dropdowns
2. Choose a tropical cyclone from the populated list
3. Select a classification standard (Atlantic or Taiwan)
4. Click "Generate Animation"
5. The animation shows:
- Tropical cyclone track growing over time with colored markers based on intensity
- Wind radius circles (if data available) for 34kt (blue), 50kt (orange), and 64kt (red)
- Date/time on the bottom left
- Details sidebar showing name, date, wind speed, pressure, category, and wind radii
- Color legend for storm categories and wind radii
Note: Wind radius data may not be available for all storms or all observation times.
Different agencies use different wind speed averaging periods: USA (1-min), JTWC (1-min), JMA (10-min), IMD (3-min).
""")
year_dropdown.change(fn=update_typhoon_options, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
basin_dropdown.change(fn=update_typhoon_options, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
animate_btn.click(
fn=simplified_track_video,
inputs=[year_dropdown, basin_dropdown, typhoon_dropdown, standard_dropdown],
outputs=path_video
)
with gr.Tab("TSNE Cluster"):
with gr.Row():
tsne_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
tsne_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
tsne_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
tsne_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=12)
tsne_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
tsne_season = gr.Dropdown(label="Season", choices=['all', 'summer', 'winter'], value='all')
tsne_analyze_btn = gr.Button("Analyze")
tsne_plot = gr.Plot(label="t-SNE Clusters")
routes_plot = gr.Plot(label="Typhoon Routes with Mean Routes")
stats_plot = gr.Plot(label="Cluster Statistics")
cluster_info = gr.Textbox(label="Cluster Information", lines=10)
tsne_analyze_btn.click(
fn=update_route_clusters,
inputs=[tsne_start_year, tsne_start_month, tsne_end_year, tsne_end_month, tsne_enso_phase, tsne_season],
outputs=[tsne_plot, routes_plot, stats_plot, cluster_info]
)
demo.launch(share=True)