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') LOCAL_iBtrace_PATH = os.path.join(DATA_PATH, 'ibtracs.ALL.list.v04r01.csv') iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.ALL.list.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) all_basins_path = os.path.join(DATA_PATH, 'ibtracs.ALL.list.v04r01.csv') try: if os.path.exists(all_basins_path): print("Loading ALL basins file...") ibtracs = tracks.TrackDataset(source='ibtracs', ibtracs_url=all_basins_path) else: print("Downloading ALL basins file...") response = requests.get(iBtrace_uri) response.raise_for_status() with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file: temp_file.write(response.text) shutil.move(temp_file.name, all_basins_path) print(f"Downloaded {all_basins_path}. Verify it contains all basins.") ibtracs = tracks.TrackDataset(source='ibtracs', ibtracs_url=all_basins_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_iBtrace_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)