euler314 commited on
Commit
a60f1c0
·
verified ·
1 Parent(s): 1ca7717

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +300 -227
app.py CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  import numpy as np
@@ -9,45 +18,57 @@ import cartopy.feature as cfeature
9
  import plotly.graph_objects as go
10
  import plotly.express as px
11
  from plotly.subplots import make_subplots
12
- import pickle
13
- import requests
14
- import os
15
- import argparse
16
- from datetime import datetime
17
- import statsmodels.api as sm
18
- import shutil
19
- import tempfile
20
- import csv
21
- from collections import defaultdict
22
  from sklearn.manifold import TSNE
23
  from sklearn.cluster import DBSCAN
 
24
  from scipy.interpolate import interp1d
25
 
26
- # Import tropycal for IBTrACS processing (for typhoon options)
 
 
 
 
 
 
 
 
 
 
27
  import tropycal.tracks as tracks
28
 
29
- # ------------------ Argument Parsing ------------------
 
 
 
 
 
 
 
30
  parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
31
  parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
32
  args = parser.parse_args()
33
  DATA_PATH = args.data_path
34
 
35
- # ------------------ File Paths ------------------
36
  ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
37
  TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
 
38
 
39
- # ------------------ IBTrACS Files (for typhoon options) ------------------
 
 
 
 
40
  BASIN_FILES = {
41
  'EP': 'ibtracs.EP.list.v04r01.csv',
42
  'NA': 'ibtracs.NA.list.v04r01.csv',
43
  'WP': 'ibtracs.WP.list.v04r01.csv'
44
  }
45
- IBTRACS_BASE_URL = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/'
46
-
47
- CACHE_FILE = 'ibtracs_cache.pkl'
48
- CACHE_EXPIRY_DAYS = 0 # Force refresh for testing
49
 
50
- # ------------------ Color Maps and Standards ------------------
 
 
51
  color_map = {
52
  'C5 Super Typhoon': 'rgb(255, 0, 0)',
53
  'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
@@ -73,7 +94,9 @@ taiwan_standard = {
73
  'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
74
  }
75
 
76
- # ------------------ Season and Regions ------------------
 
 
77
  season_months = {
78
  'all': list(range(1, 13)),
79
  'summer': [6, 7, 8],
@@ -88,7 +111,9 @@ regions = {
88
  "Philippines": {"lat_min": 5, "lat_max": 21, "lon_min": 115, "lon_max": 130}
89
  }
90
 
91
- # ------------------ ONI and Typhoon Data Functions ------------------
 
 
92
  def download_oni_file(url, filename):
93
  response = requests.get(url)
94
  response.raise_for_status()
@@ -98,8 +123,8 @@ def download_oni_file(url, filename):
98
 
99
  def convert_oni_ascii_to_csv(input_file, output_file):
100
  data = defaultdict(lambda: [''] * 12)
101
- season_to_month = {'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
102
- 'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11}
103
  with open(input_file, 'r') as f:
104
  lines = f.readlines()[1:]
105
  for line in lines:
@@ -109,11 +134,11 @@ def convert_oni_ascii_to_csv(input_file, output_file):
109
  if season in season_to_month:
110
  month = season_to_month[season]
111
  if season == 'DJF':
112
- year = str(int(year) - 1)
113
  data[year][month-1] = anom
114
  with open(output_file, 'w', newline='') as f:
115
  writer = csv.writer(f)
116
- writer.writerow(['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
117
  for year in sorted(data.keys()):
118
  writer.writerow([year] + data[year])
119
 
@@ -138,10 +163,10 @@ def load_data(oni_path, typhoon_path):
138
 
139
  def process_oni_data(oni_data):
140
  oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
141
- month_map = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
142
- 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'}
143
  oni_long['Month'] = oni_long['Month'].map(month_map)
144
- oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str) + '-' + oni_long['Month'] + '-01')
145
  oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
146
  return oni_long
147
 
@@ -150,10 +175,10 @@ def process_typhoon_data(typhoon_data):
150
  typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
151
  typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
152
  typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
153
- print(f"Unique basins in typhoon_data: {typhoon_data['SID'].str[:2].unique()}")
154
  typhoon_max = typhoon_data.groupby('SID').agg({
155
- 'USA_WIND': 'max', 'USA_PRES': 'min', 'ISO_TIME': 'first', 'SEASON': 'first', 'NAME': 'first',
156
- 'LAT': 'first', 'LON': 'first'
157
  }).reset_index()
158
  typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
159
  typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
@@ -161,7 +186,7 @@ def process_typhoon_data(typhoon_data):
161
  return typhoon_max
162
 
163
  def merge_data(oni_long, typhoon_max):
164
- return pd.merge(typhoon_max, oni_long, on=['Year', 'Month'])
165
 
166
  def categorize_typhoon(wind_speed):
167
  if wind_speed >= 137:
@@ -189,12 +214,12 @@ def classify_enso_phases(oni_value):
189
  else:
190
  return 'Neutral'
191
 
192
- # ------------------ Regression Functions ------------------
193
  def perform_wind_regression(start_year, start_month, end_year, end_month):
194
  start_date = datetime(start_year, start_month, 1)
195
  end_date = datetime(end_year, end_month, 28)
196
- data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['USA_WIND', 'ONI'])
197
- data['severe_typhoon'] = (data['USA_WIND'] >= 64).astype(int)
198
  X = sm.add_constant(data['ONI'])
199
  y = data['severe_typhoon']
200
  model = sm.Logit(y, X).fit(disp=0)
@@ -206,8 +231,8 @@ def perform_wind_regression(start_year, start_month, end_year, end_month):
206
  def perform_pressure_regression(start_year, start_month, end_year, end_month):
207
  start_date = datetime(start_year, start_month, 1)
208
  end_date = datetime(end_year, end_month, 28)
209
- data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['USA_PRES', 'ONI'])
210
- data['intense_typhoon'] = (data['USA_PRES'] <= 950).astype(int)
211
  X = sm.add_constant(data['ONI'])
212
  y = data['intense_typhoon']
213
  model = sm.Logit(y, X).fit(disp=0)
@@ -219,54 +244,54 @@ def perform_pressure_regression(start_year, start_month, end_year, end_month):
219
  def perform_longitude_regression(start_year, start_month, end_year, end_month):
220
  start_date = datetime(start_year, start_month, 1)
221
  end_date = datetime(end_year, end_month, 28)
222
- data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['LON', 'ONI'])
223
- data['western_typhoon'] = (data['LON'] <= 140).astype(int)
224
  X = sm.add_constant(data['ONI'])
225
  y = data['western_typhoon']
226
- model = sm.Logit(y, X).fit(disp=0)
227
  beta_1 = model.params['ONI']
228
  exp_beta_1 = np.exp(beta_1)
229
  p_value = model.pvalues['ONI']
230
  return f"Longitude Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
231
 
232
- # ------------------ IBTrACS Data Loading (for typhoon options) ------------------
233
  def load_ibtracs_data():
234
  ibtracs_data = {}
235
  for basin, filename in BASIN_FILES.items():
236
  local_path = os.path.join(DATA_PATH, filename)
237
  if not os.path.exists(local_path):
238
- print(f"Downloading {basin} basin file...")
239
- response = requests.get(IBTRACS_BASE_URL + filename)
240
  response.raise_for_status()
241
  with open(local_path, 'wb') as f:
242
  f.write(response.content)
243
- print(f"Downloaded {basin} basin file.")
244
  try:
245
- print(f"--> Starting to read in IBTrACS data for basin {basin}")
246
  ds = tracks.TrackDataset(source='ibtracs', ibtracs_url=local_path)
247
- print(f"--> Completed reading in IBTrACS data for basin {basin}")
248
  ibtracs_data[basin] = ds
249
  except ValueError as e:
250
- print(f"Warning: Skipping basin {basin} due to error: {e}")
251
  ibtracs_data[basin] = None
252
  return ibtracs_data
253
 
254
  ibtracs = load_ibtracs_data()
255
 
256
- # ------------------ Load and Process Data ------------------
257
  update_oni_data()
258
  oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
259
  oni_long = process_oni_data(oni_data)
260
  typhoon_max = process_typhoon_data(typhoon_data)
261
  merged_data = merge_data(oni_long, typhoon_max)
262
 
263
- # ------------------ Visualization Functions ------------------
264
  def generate_typhoon_tracks(filtered_data, typhoon_search):
265
  fig = go.Figure()
266
  for sid in filtered_data['SID'].unique():
267
  storm_data = filtered_data[filtered_data['SID'] == sid]
268
  phase = storm_data['ENSO_Phase'].iloc[0]
269
- color = {'El Nino': 'red', 'La Nina': 'blue', 'Neutral': 'green'}.get(phase, 'black')
270
  fig.add_trace(go.Scattergeo(
271
  lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
272
  name=storm_data['NAME'].iloc[0], line=dict(width=2, color=color)
@@ -287,32 +312,36 @@ def generate_typhoon_tracks(filtered_data, typhoon_search):
287
  return fig
288
 
289
  def generate_wind_oni_scatter(filtered_data, typhoon_search):
290
- fig = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category', hover_data=['NAME', 'Year', 'Category'],
291
- title='Wind Speed vs ONI', labels={'ONI': 'ONI Value', 'USA_WIND': 'Max Wind Speed (knots)'},
 
 
292
  color_discrete_map=color_map)
293
  if typhoon_search:
294
  mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
295
  if mask.any():
296
  fig.add_trace(go.Scatter(
297
- x=filtered_data.loc[mask, 'ONI'], y=filtered_data.loc[mask, 'USA_WIND'],
298
  mode='markers', marker=dict(size=10, color='red', symbol='star'),
299
  name=f'Matched: {typhoon_search}',
300
- text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')'
301
  ))
302
  return fig
303
 
304
  def generate_pressure_oni_scatter(filtered_data, typhoon_search):
305
- fig = px.scatter(filtered_data, x='ONI', y='USA_PRES', color='Category', hover_data=['NAME', 'Year', 'Category'],
306
- title='Pressure vs ONI', labels={'ONI': 'ONI Value', 'USA_PRES': 'Min Pressure (hPa)'},
 
 
307
  color_discrete_map=color_map)
308
  if typhoon_search:
309
  mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
310
  if mask.any():
311
  fig.add_trace(go.Scatter(
312
- x=filtered_data.loc[mask, 'ONI'], y=filtered_data.loc[mask, 'USA_PRES'],
313
  mode='markers', marker=dict(size=10, color='red', symbol='star'),
314
  name=f'Matched: {typhoon_search}',
315
- text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')'
316
  ))
317
  return fig
318
 
@@ -320,7 +349,7 @@ def generate_regression_analysis(filtered_data):
320
  fig = px.scatter(filtered_data, x='LON', y='ONI', hover_data=['NAME'],
321
  title='Typhoon Generation Longitude vs ONI (All Years)')
322
  if len(filtered_data) > 1:
323
- X = np.array(filtered_data['LON']).reshape(-1, 1)
324
  y = filtered_data['ONI']
325
  model = sm.OLS(y, sm.add_constant(X)).fit()
326
  y_pred = model.predict(sm.add_constant(X))
@@ -334,7 +363,7 @@ def generate_regression_analysis(filtered_data):
334
  def generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
335
  start_date = datetime(start_year, start_month, 1)
336
  end_date = datetime(end_year, end_month, 28)
337
- filtered_data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].copy()
338
  filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
339
  if enso_phase != 'all':
340
  filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
@@ -347,7 +376,7 @@ def generate_main_analysis(start_year, start_month, end_year, end_month, enso_ph
347
  def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
348
  start_date = datetime(start_year, start_month, 1)
349
  end_date = datetime(end_year, end_month, 28)
350
- filtered_data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].copy()
351
  filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
352
  if enso_phase != 'all':
353
  filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
@@ -355,27 +384,25 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
355
  count = len(unique_storms)
356
  fig = go.Figure()
357
  for sid in unique_storms:
358
- storm_data = typhoon_data[typhoon_data['SID'] == sid]
359
  name = storm_data['NAME'].iloc[0] if pd.notnull(storm_data['NAME'].iloc[0]) else "Unnamed"
360
- storm_oni = filtered_data[filtered_data['SID'] == sid]['ONI'].iloc[0]
361
- color = 'red' if storm_oni >= 0.5 else ('blue' if storm_oni <= -0.5 else 'green')
362
  fig.add_trace(go.Scattergeo(
363
  lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
364
  name=f"{name} ({storm_data['SEASON'].iloc[0]})",
365
- line=dict(width=1.5, color=color),
366
- hoverinfo="name"
367
  ))
368
  if typhoon_search:
369
  search_mask = typhoon_data['NAME'].str.contains(typhoon_search, case=False, na=False)
370
  if search_mask.any():
371
  for sid in typhoon_data[search_mask]['SID'].unique():
372
- storm_data = typhoon_data[typhoon_data['SID'] == sid]
373
  fig.add_trace(go.Scattergeo(
374
  lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines+markers',
375
  name=f"MATCHED: {storm_data['NAME'].iloc[0]} ({storm_data['SEASON'].iloc[0]})",
376
  line=dict(width=3, color='yellow'),
377
- marker=dict(size=5),
378
- hoverinfo="name"
379
  ))
380
  fig.update_layout(
381
  title=f"Typhoon Tracks ({start_year}-{start_month} to {end_year}-{end_month})",
@@ -383,9 +410,9 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
383
  projection_type='natural earth',
384
  showland=True,
385
  showcoastlines=True,
386
- landcolor='rgb(243, 243, 243)',
387
- countrycolor='rgb(204, 204, 204)',
388
- coastlinecolor='rgb(204, 204, 204)',
389
  center=dict(lon=140, lat=20),
390
  projection_scale=3
391
  ),
@@ -416,8 +443,8 @@ def get_longitude_analysis(start_year, start_month, end_year, end_month, enso_ph
416
  regression = perform_longitude_regression(start_year, start_month, end_year, end_month)
417
  return results[3], results[4], regression
418
 
419
- def categorize_typhoon_by_standard(wind_speed, standard):
420
- if standard == 'taiwan':
421
  wind_speed_ms = wind_speed * 0.514444
422
  if wind_speed_ms >= 51.0:
423
  return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['hex']
@@ -441,11 +468,120 @@ def categorize_typhoon_by_standard(wind_speed, standard):
441
  return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
442
  return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
443
 
444
- # ------------------ Animation Functions Using Processed CSV ------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  def generate_track_video_from_csv(year, storm_id, standard):
446
  storm_df = typhoon_data[typhoon_data['SID'] == storm_id].copy()
447
  if storm_df.empty:
448
- print("No data found for storm:", storm_id)
449
  return None
450
  storm_df = storm_df.sort_values('ISO_TIME')
451
  lats = storm_df['LAT'].astype(float).values
@@ -460,42 +596,41 @@ def generate_track_video_from_csv(year, storm_id, standard):
460
 
461
  min_lat, max_lat = np.min(lats), np.max(lats)
462
  min_lon, max_lon = np.min(lons), np.max(lons)
463
- lat_padding = max((max_lat - min_lat) * 0.3, 5)
464
- lon_padding = max((max_lon - min_lon) * 0.3, 5)
465
 
466
- fig = plt.figure(figsize=(12, 9), dpi=100)
467
- ax = plt.axes([0.05, 0.05, 0.60, 0.90],
468
- projection=ccrs.PlateCarree(central_longitude=-25))
 
469
  ax.set_extent([min_lon - lon_padding, max_lon + lon_padding, min_lat - lat_padding, max_lat + lat_padding],
470
  crs=ccrs.PlateCarree())
 
 
 
 
471
 
472
- ax.add_feature(cfeature.LAND, facecolor='lightgray')
473
- ax.add_feature(cfeature.OCEAN, facecolor='lightblue')
474
- ax.add_feature(cfeature.COASTLINE, edgecolor='black')
475
- ax.add_feature(cfeature.BORDERS, linestyle=':', edgecolor='gray')
476
- ax.gridlines(draw_labels=True, linestyle='--', color='gray', alpha=0.5)
477
- ax.set_title(f"{year} {storm_name} - {season}", fontsize=16)
478
-
479
- line, = ax.plot([], [], 'b-', linewidth=2, transform=ccrs.PlateCarree())
480
- point, = ax.plot([], [], 'o', markersize=10, transform=ccrs.PlateCarree())
481
- date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=12,
482
- bbox=dict(facecolor='white', alpha=0.8))
483
- # Dynamic state display at right side
484
- state_text = fig.text(0.70, 0.60, '', fontsize=14, verticalalignment='top',
485
- bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
486
 
487
- # Persistent legend for category colors
488
- legend_elements = [plt.Line2D([0], [0], marker='o', color='w', label=f"{cat}",
489
- markerfacecolor=details['hex'], markersize=10)
490
- for cat, details in (atlantic_standard if standard=='atlantic' else taiwan_standard).items()]
491
- ax.legend(handles=legend_elements, title="Storm Categories", loc='upper right', fontsize=10)
 
 
492
 
493
  def init():
494
  line.set_data([], [])
495
  point.set_data([], [])
496
  date_text.set_text('')
497
- state_text.set_text('')
498
- return line, point, date_text, state_text
499
 
500
  def update(frame):
501
  line.set_data(lons[:frame+1], lats[:frame+1])
@@ -505,12 +640,12 @@ def generate_track_video_from_csv(year, storm_id, standard):
505
  point.set_color(color)
506
  dt_str = pd.to_datetime(times[frame]).strftime('%Y-%m-%d %H:%M')
507
  date_text.set_text(dt_str)
508
- state_info = (f"Name: {storm_name}\n"
509
- f"Date: {dt_str}\n"
510
- f"Wind: {wind_speed:.1f} kt\n"
511
- f"Category: {category}")
512
- state_text.set_text(state_info)
513
- return line, point, date_text, state_text
514
 
515
  ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(times),
516
  interval=200, blit=True, repeat=True)
@@ -526,7 +661,7 @@ def simplified_track_video(year, basin, typhoon, standard):
526
  storm_id = typhoon.split('(')[-1].strip(')')
527
  return generate_track_video_from_csv(year, storm_id, standard)
528
 
529
- # ------------------ Typhoon Options Update Functions ------------------
530
  basin_to_prefix = {
531
  "All Basins": "all",
532
  "NA - North Atlantic": "NA",
@@ -545,18 +680,18 @@ def update_typhoon_options(year, basin):
545
  continue
546
  summaries.append(season_data.summary())
547
  if len(summaries) == 0:
548
- print("Error updating typhoon options: No storms identified for the given year and basin.")
549
  return gr.update(choices=[], value=None)
550
  combined_summary = pd.concat(summaries, ignore_index=True)
551
  else:
552
  prefix = basin_to_prefix.get(basin)
553
  ds = ibtracs.get(prefix)
554
  if ds is None:
555
- print("Error updating typhoon options: Dataset not found for the given basin.")
556
  return gr.update(choices=[], value=None)
557
  season_data = ds.get_season(int(year))
558
  if season_data.summary().empty:
559
- print("Error updating typhoon options: No storms identified for the given year and basin.")
560
  return gr.update(choices=[], value=None)
561
  combined_summary = season_data.summary()
562
  options = []
@@ -569,17 +704,16 @@ def update_typhoon_options(year, basin):
569
  continue
570
  return gr.update(choices=options, value=options[0] if options else None)
571
  except Exception as e:
572
- print(f"Error updating typhoon options: {e}")
573
  return gr.update(choices=[], value=None)
574
 
575
  def update_typhoon_options_anim(year, basin):
576
  try:
577
- # For animation, use the processed CSV data (without filtering by a specific prefix)
578
  data = typhoon_data.copy()
579
- data['Year'] = pd.to_datetime(data['ISO_TIME']).dt.year
580
  season_data = data[data['Year'] == int(year)]
581
  if season_data.empty:
582
- print("Error updating typhoon options (animation): No storms identified for the given year.")
583
  return gr.update(choices=[], value=None)
584
  summary = season_data.groupby('SID').first().reset_index()
585
  options = []
@@ -588,188 +722,127 @@ def update_typhoon_options_anim(year, basin):
588
  options.append(f"{name} ({row['SID']})")
589
  return gr.update(choices=options, value=options[0] if options else None)
590
  except Exception as e:
591
- print(f"Error updating typhoon options (animation): {e}")
592
  return gr.update(choices=[], value=None)
593
 
594
- # ------------------ TSNE Cluster Function ------------------
595
- def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
596
- # Use only WP storms from processed CSV for clustering.
597
- wp_data = typhoon_data[typhoon_data['SID'].str.startswith("WP")]
598
- if wp_data.empty:
599
- return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No West Pacific storms found."
600
- wp_data['Year'] = pd.to_datetime(wp_data['ISO_TIME']).dt.year
601
- wp_season = wp_data[wp_data['Year'] == int(start_year)]
602
- if wp_season.empty:
603
- return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No storms found for the given period in WP."
604
-
605
- all_storms_data = []
606
- for sid, group in wp_data.groupby('SID'):
607
- group = group.sort_values('ISO_TIME')
608
- times = pd.to_datetime(group['ISO_TIME']).values
609
- lats = group['LAT'].astype(float).values
610
- lons = group['LON'].astype(float).values
611
- if len(lons) < 2:
612
- continue
613
- if season != 'all':
614
- month = pd.to_datetime(group['ISO_TIME']).iloc[0].month
615
- if season == 'summer' and not (4 <= month <= 8):
616
- continue
617
- if season == 'winter' and not (9 <= month <= 12):
618
- continue
619
- all_storms_data.append((lons, lats, np.array(group['USA_WIND'].astype(float)), times, sid))
620
- if not all_storms_data:
621
- return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid WP storms for clustering."
622
-
623
- max_length = max(len(item[0]) for item in all_storms_data)
624
- route_vectors = []
625
- filtered_storms = []
626
- for lons, lats, winds, times, sid in all_storms_data:
627
- t = np.linspace(0, 1, len(lons))
628
- t_new = np.linspace(0, 1, max_length)
629
- try:
630
- lon_i = interp1d(t, lons, kind='linear', fill_value='extrapolate')(t_new)
631
- lat_i = interp1d(t, lats, kind='linear', fill_value='extrapolate')(t_new)
632
- except Exception:
633
- continue
634
- route_vector = np.column_stack((lon_i, lat_i)).flatten()
635
- if np.isnan(route_vector).any():
636
- continue
637
- route_vectors.append(route_vector)
638
- filtered_storms.append((lon_i, lat_i, winds, times, sid))
639
- route_vectors = np.array(route_vectors)
640
- if len(route_vectors) == 0:
641
- return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms after interpolation."
642
-
643
- tsne = TSNE(n_components=2, random_state=42, verbose=1)
644
- tsne_results = tsne.fit_transform(route_vectors)
645
- dbscan = DBSCAN(eps=5, min_samples=3)
646
- best_labels = dbscan.fit_predict(tsne_results)
647
- unique_labels = sorted(set(best_labels) - {-1})
648
- fig_tsne = go.Figure()
649
- colors = px.colors.qualitative.Safe
650
- for i, label in enumerate(unique_labels):
651
- indices = np.where(best_labels == label)[0]
652
- fig_tsne.add_trace(go.Scatter(
653
- x=tsne_results[indices, 0],
654
- y=tsne_results[indices, 1],
655
- mode='markers',
656
- marker=dict(color=colors[i % len(colors)]),
657
- name=f"Cluster {label}"
658
- ))
659
- fig_tsne.update_layout(title="t-SNE of WP Storm Routes")
660
- fig_routes = go.Figure() # Placeholder
661
- fig_stats = make_subplots(rows=2, cols=1) # Placeholder
662
- info = "TSNE clustering complete."
663
- return fig_tsne, fig_routes, fig_stats, info
664
-
665
- # ------------------ Gradio Interface ------------------
666
  with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
667
  gr.Markdown("# Typhoon Analysis Dashboard")
668
 
669
  with gr.Tab("Overview"):
670
  gr.Markdown("""
671
  ## Welcome to the Typhoon Analysis Dashboard
672
-
673
  This dashboard allows you to analyze typhoon data in relation to ENSO phases.
674
-
675
  ### Features:
676
- - **Track Visualization**: View typhoon tracks by time period and ENSO phase (all basins available)
677
- - **Wind Analysis**: Examine wind speed vs ONI relationships
678
- - **Pressure Analysis**: Analyze pressure vs ONI relationships
679
- - **Longitude Analysis**: Study typhoon generation longitude vs ONI
680
- - **Path Animation**: Watch animated tropical cyclone paths with a dynamic state display and persistent legend (using processed CSV data)
681
- - **TSNE Cluster**: Perform t-SNE clustering on WP storm routes with mean routes and region analysis
682
-
683
- Select a tab above to begin your analysis.
684
  """)
685
 
686
  with gr.Tab("Track Visualization"):
687
  with gr.Row():
688
  start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
689
- start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
690
  end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
691
- end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
692
- enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
693
  typhoon_search = gr.Textbox(label="Typhoon Search")
694
  analyze_btn = gr.Button("Generate Tracks")
695
  tracks_plot = gr.Plot(label="Typhoon Tracks", elem_id="tracks_plot")
696
  typhoon_count = gr.Textbox(label="Number of Typhoons Displayed")
697
- 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])
 
 
698
 
699
  with gr.Tab("Wind Analysis"):
700
  with gr.Row():
701
  wind_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
702
- wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
703
  wind_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
704
- wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
705
- wind_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
706
  wind_typhoon_search = gr.Textbox(label="Typhoon Search")
707
  wind_analyze_btn = gr.Button("Generate Wind Analysis")
708
  wind_scatter = gr.Plot(label="Wind Speed vs ONI")
709
  wind_regression_results = gr.Textbox(label="Wind Regression Results")
710
- 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])
 
 
711
 
712
  with gr.Tab("Pressure Analysis"):
713
  with gr.Row():
714
  pressure_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
715
- pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
716
  pressure_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
717
- pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
718
- pressure_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
719
  pressure_typhoon_search = gr.Textbox(label="Typhoon Search")
720
  pressure_analyze_btn = gr.Button("Generate Pressure Analysis")
721
  pressure_scatter = gr.Plot(label="Pressure vs ONI")
722
  pressure_regression_results = gr.Textbox(label="Pressure Regression Results")
723
- 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])
 
 
724
 
725
  with gr.Tab("Longitude Analysis"):
726
  with gr.Row():
727
  lon_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
728
- lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
729
- lon_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
730
- lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
731
- lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
732
  lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
733
  lon_analyze_btn = gr.Button("Generate Longitude Analysis")
734
  regression_plot = gr.Plot(label="Longitude vs ONI")
735
  slopes_text = gr.Textbox(label="Regression Slopes")
736
  lon_regression_results = gr.Textbox(label="Longitude Regression Results")
737
- 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])
 
 
738
 
739
  with gr.Tab("Tropical Cyclone Path Animation"):
740
  with gr.Row():
741
- year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950, 2025)], value="2000")
742
- basin_dropdown = gr.Dropdown(label="Basin", choices=["NA - North Atlantic", "EP - Eastern North Pacific", "WP - Western North Pacific", "All Basins"], value="NA - North Atlantic")
743
  with gr.Row():
744
  typhoon_dropdown = gr.Dropdown(label="Tropical Cyclone")
745
- standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
746
  animate_btn = gr.Button("Generate Animation")
747
  path_video = gr.Video(label="Tropical Cyclone Path Animation", format="mp4", interactive=False, elem_id="path_video")
748
  animation_info = gr.Markdown("""
749
  ### Animation Instructions
750
- 1. Select a year and basin from the dropdowns. (This animation uses processed CSV data.)
751
  2. Choose a tropical cyclone from the populated list.
752
  3. Select a classification standard (Atlantic or Taiwan).
753
  4. Click "Generate Animation".
754
- 5. The animation displays the storm track along with a dynamic sidebar that shows the current state (name, date, wind, category) and a persistent legend for colors.
755
  """)
756
  year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
757
  basin_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
758
- animate_btn.click(fn=simplified_track_video, inputs=[year_dropdown, basin_dropdown, typhoon_dropdown, standard_dropdown], outputs=path_video)
 
 
759
 
760
  with gr.Tab("TSNE Cluster"):
761
  with gr.Row():
762
  tsne_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
763
- tsne_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
764
  tsne_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
765
- tsne_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=12)
766
- tsne_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
767
- tsne_season = gr.Dropdown(label="Season", choices=['all', 'summer', 'winter'], value='all')
768
  tsne_analyze_btn = gr.Button("Analyze")
769
  tsne_plot = gr.Plot(label="t-SNE Clusters")
770
  routes_plot = gr.Plot(label="Typhoon Routes with Mean Routes")
771
  stats_plot = gr.Plot(label="Cluster Statistics")
772
  cluster_info = gr.Textbox(label="Cluster Information", lines=10)
773
- 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])
 
 
774
 
775
  demo.launch(share=True)
 
1
+ import os
2
+ import argparse
3
+ import logging
4
+ import pickle
5
+ import threading
6
+ import time
7
+ from datetime import datetime, timedelta
8
+ from collections import defaultdict
9
+
10
  import gradio as gr
11
  import pandas as pd
12
  import numpy as np
 
18
  import plotly.graph_objects as go
19
  import plotly.express as px
20
  from plotly.subplots import make_subplots
21
+
 
 
 
 
 
 
 
 
 
22
  from sklearn.manifold import TSNE
23
  from sklearn.cluster import DBSCAN
24
+ from sklearn.preprocessing import StandardScaler
25
  from scipy.interpolate import interp1d
26
 
27
+ import requests
28
+ import tempfile
29
+ import shutil
30
+ import xarray as xr
31
+
32
+ try:
33
+ import cdsapi
34
+ CDSAPI_AVAILABLE = True
35
+ except ImportError:
36
+ CDSAPI_AVAILABLE = False
37
+
38
  import tropycal.tracks as tracks
39
 
40
+ # -----------------------------
41
+ # Configuration and Setup
42
+ # -----------------------------
43
+ logging.basicConfig(
44
+ level=logging.INFO, # Use DEBUG for more details
45
+ format='%(asctime)s - %(levelname)s - %(message)s'
46
+ )
47
+
48
  parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
49
  parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
50
  args = parser.parse_args()
51
  DATA_PATH = args.data_path
52
 
53
+ # Data paths
54
  ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
55
  TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
56
+ MERGED_DATA_CSV = os.path.join(DATA_PATH, 'merged_typhoon_era5_data.csv') # used by other analyses
57
 
58
+ # IBTrACS (used only for typhoon option updates)
59
+ LOCAL_IBTRACS_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
60
+ IBTRACS_URI = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.WP.list.v04r01.csv'
61
+ CACHE_FILE = os.path.join(DATA_PATH, 'ibtracs_cache.pkl')
62
+ CACHE_EXPIRY_DAYS = 1
63
  BASIN_FILES = {
64
  'EP': 'ibtracs.EP.list.v04r01.csv',
65
  'NA': 'ibtracs.NA.list.v04r01.csv',
66
  'WP': 'ibtracs.WP.list.v04r01.csv'
67
  }
 
 
 
 
68
 
69
+ # -----------------------------
70
+ # Color Maps and Standards
71
+ # -----------------------------
72
  color_map = {
73
  'C5 Super Typhoon': 'rgb(255, 0, 0)',
74
  'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
 
94
  'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
95
  }
96
 
97
+ # -----------------------------
98
+ # Season and Regions
99
+ # -----------------------------
100
  season_months = {
101
  'all': list(range(1, 13)),
102
  'summer': [6, 7, 8],
 
111
  "Philippines": {"lat_min": 5, "lat_max": 21, "lon_min": 115, "lon_max": 130}
112
  }
113
 
114
+ # -----------------------------
115
+ # ONI and Typhoon Data Functions
116
+ # -----------------------------
117
  def download_oni_file(url, filename):
118
  response = requests.get(url)
119
  response.raise_for_status()
 
123
 
124
  def convert_oni_ascii_to_csv(input_file, output_file):
125
  data = defaultdict(lambda: [''] * 12)
126
+ season_to_month = {'DJF':12, 'JFM':1, 'FMA':2, 'MAM':3, 'AMJ':4, 'MJJ':5,
127
+ 'JJA':6, 'JAS':7, 'ASO':8, 'SON':9, 'OND':10, 'NDJ':11}
128
  with open(input_file, 'r') as f:
129
  lines = f.readlines()[1:]
130
  for line in lines:
 
134
  if season in season_to_month:
135
  month = season_to_month[season]
136
  if season == 'DJF':
137
+ year = str(int(year)-1)
138
  data[year][month-1] = anom
139
  with open(output_file, 'w', newline='') as f:
140
  writer = csv.writer(f)
141
+ writer.writerow(['Year','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])
142
  for year in sorted(data.keys()):
143
  writer.writerow([year] + data[year])
144
 
 
163
 
164
  def process_oni_data(oni_data):
165
  oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
166
+ month_map = {'Jan':'01','Feb':'02','Mar':'03','Apr':'04','May':'05','Jun':'06',
167
+ 'Jul':'07','Aug':'08','Sep':'09','Oct':'10','Nov':'11','Dec':'12'}
168
  oni_long['Month'] = oni_long['Month'].map(month_map)
169
+ oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str)+'-'+oni_long['Month']+'-01')
170
  oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
171
  return oni_long
172
 
 
175
  typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
176
  typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
177
  typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
178
+ logging.info(f"Unique basins in typhoon_data: {typhoon_data['SID'].str[:2].unique()}")
179
  typhoon_max = typhoon_data.groupby('SID').agg({
180
+ 'USA_WIND':'max','USA_PRES':'min','ISO_TIME':'first','SEASON':'first','NAME':'first',
181
+ 'LAT':'first','LON':'first'
182
  }).reset_index()
183
  typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
184
  typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
 
186
  return typhoon_max
187
 
188
  def merge_data(oni_long, typhoon_max):
189
+ return pd.merge(typhoon_max, oni_long, on=['Year','Month'])
190
 
191
  def categorize_typhoon(wind_speed):
192
  if wind_speed >= 137:
 
214
  else:
215
  return 'Neutral'
216
 
217
+ # ------------- Regression Functions -------------
218
  def perform_wind_regression(start_year, start_month, end_year, end_month):
219
  start_date = datetime(start_year, start_month, 1)
220
  end_date = datetime(end_year, end_month, 28)
221
+ data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].dropna(subset=['USA_WIND','ONI'])
222
+ data['severe_typhoon'] = (data['USA_WIND']>=64).astype(int)
223
  X = sm.add_constant(data['ONI'])
224
  y = data['severe_typhoon']
225
  model = sm.Logit(y, X).fit(disp=0)
 
231
  def perform_pressure_regression(start_year, start_month, end_year, end_month):
232
  start_date = datetime(start_year, start_month, 1)
233
  end_date = datetime(end_year, end_month, 28)
234
+ data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].dropna(subset=['USA_PRES','ONI'])
235
+ data['intense_typhoon'] = (data['USA_PRES']<=950).astype(int)
236
  X = sm.add_constant(data['ONI'])
237
  y = data['intense_typhoon']
238
  model = sm.Logit(y, X).fit(disp=0)
 
244
  def perform_longitude_regression(start_year, start_month, end_year, end_month):
245
  start_date = datetime(start_year, start_month, 1)
246
  end_date = datetime(end_year, end_month, 28)
247
+ data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].dropna(subset=['LON','ONI'])
248
+ data['western_typhoon'] = (data['LON']<=140).astype(int)
249
  X = sm.add_constant(data['ONI'])
250
  y = data['western_typhoon']
251
+ model = sm.OLS(y, sm.add_constant(X)).fit()
252
  beta_1 = model.params['ONI']
253
  exp_beta_1 = np.exp(beta_1)
254
  p_value = model.pvalues['ONI']
255
  return f"Longitude Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
256
 
257
+ # ------------- IBTrACS Data Loading -------------
258
  def load_ibtracs_data():
259
  ibtracs_data = {}
260
  for basin, filename in BASIN_FILES.items():
261
  local_path = os.path.join(DATA_PATH, filename)
262
  if not os.path.exists(local_path):
263
+ logging.info(f"Downloading {basin} basin file...")
264
+ response = requests.get(IBTRACS_BASE_URI+filename)
265
  response.raise_for_status()
266
  with open(local_path, 'wb') as f:
267
  f.write(response.content)
268
+ logging.info(f"Downloaded {basin} basin file.")
269
  try:
270
+ logging.info(f"--> Starting to read in IBTrACS data for basin {basin}")
271
  ds = tracks.TrackDataset(source='ibtracs', ibtracs_url=local_path)
272
+ logging.info(f"--> Completed reading in IBTrACS data for basin {basin}")
273
  ibtracs_data[basin] = ds
274
  except ValueError as e:
275
+ logging.warning(f"Skipping basin {basin} due to error: {e}")
276
  ibtracs_data[basin] = None
277
  return ibtracs_data
278
 
279
  ibtracs = load_ibtracs_data()
280
 
281
+ # ------------- Load & Process Data -------------
282
  update_oni_data()
283
  oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
284
  oni_long = process_oni_data(oni_data)
285
  typhoon_max = process_typhoon_data(typhoon_data)
286
  merged_data = merge_data(oni_long, typhoon_max)
287
 
288
+ # ------------- Visualization Functions -------------
289
  def generate_typhoon_tracks(filtered_data, typhoon_search):
290
  fig = go.Figure()
291
  for sid in filtered_data['SID'].unique():
292
  storm_data = filtered_data[filtered_data['SID'] == sid]
293
  phase = storm_data['ENSO_Phase'].iloc[0]
294
+ color = {'El Nino':'red','La Nina':'blue','Neutral':'green'}.get(phase, 'black')
295
  fig.add_trace(go.Scattergeo(
296
  lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
297
  name=storm_data['NAME'].iloc[0], line=dict(width=2, color=color)
 
312
  return fig
313
 
314
  def generate_wind_oni_scatter(filtered_data, typhoon_search):
315
+ fig = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category',
316
+ hover_data=['NAME','Year','Category'],
317
+ title='Wind Speed vs ONI',
318
+ labels={'ONI':'ONI Value','USA_WIND':'Max Wind Speed (knots)'},
319
  color_discrete_map=color_map)
320
  if typhoon_search:
321
  mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
322
  if mask.any():
323
  fig.add_trace(go.Scatter(
324
+ x=filtered_data.loc[mask,'ONI'], y=filtered_data.loc[mask,'USA_WIND'],
325
  mode='markers', marker=dict(size=10, color='red', symbol='star'),
326
  name=f'Matched: {typhoon_search}',
327
+ text=filtered_data.loc[mask,'NAME']+' ('+filtered_data.loc[mask,'Year'].astype(str)+')'
328
  ))
329
  return fig
330
 
331
  def generate_pressure_oni_scatter(filtered_data, typhoon_search):
332
+ fig = px.scatter(filtered_data, x='ONI', y='USA_PRES', color='Category',
333
+ hover_data=['NAME','Year','Category'],
334
+ title='Pressure vs ONI',
335
+ labels={'ONI':'ONI Value','USA_PRES':'Min Pressure (hPa)'},
336
  color_discrete_map=color_map)
337
  if typhoon_search:
338
  mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
339
  if mask.any():
340
  fig.add_trace(go.Scatter(
341
+ x=filtered_data.loc[mask,'ONI'], y=filtered_data.loc[mask,'USA_PRES'],
342
  mode='markers', marker=dict(size=10, color='red', symbol='star'),
343
  name=f'Matched: {typhoon_search}',
344
+ text=filtered_data.loc[mask,'NAME']+' ('+filtered_data.loc[mask,'Year'].astype(str)+')'
345
  ))
346
  return fig
347
 
 
349
  fig = px.scatter(filtered_data, x='LON', y='ONI', hover_data=['NAME'],
350
  title='Typhoon Generation Longitude vs ONI (All Years)')
351
  if len(filtered_data) > 1:
352
+ X = np.array(filtered_data['LON']).reshape(-1,1)
353
  y = filtered_data['ONI']
354
  model = sm.OLS(y, sm.add_constant(X)).fit()
355
  y_pred = model.predict(sm.add_constant(X))
 
363
  def generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
364
  start_date = datetime(start_year, start_month, 1)
365
  end_date = datetime(end_year, end_month, 28)
366
+ filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
367
  filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
368
  if enso_phase != 'all':
369
  filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
 
376
  def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
377
  start_date = datetime(start_year, start_month, 1)
378
  end_date = datetime(end_year, end_month, 28)
379
+ filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
380
  filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
381
  if enso_phase != 'all':
382
  filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
 
384
  count = len(unique_storms)
385
  fig = go.Figure()
386
  for sid in unique_storms:
387
+ storm_data = typhoon_data[typhoon_data['SID']==sid]
388
  name = storm_data['NAME'].iloc[0] if pd.notnull(storm_data['NAME'].iloc[0]) else "Unnamed"
389
+ storm_oni = filtered_data[filtered_data['SID']==sid]['ONI'].iloc[0]
390
+ color = 'red' if storm_oni>=0.5 else ('blue' if storm_oni<=-0.5 else 'green')
391
  fig.add_trace(go.Scattergeo(
392
  lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
393
  name=f"{name} ({storm_data['SEASON'].iloc[0]})",
394
+ line=dict(width=1.5, color=color), hoverinfo="name"
 
395
  ))
396
  if typhoon_search:
397
  search_mask = typhoon_data['NAME'].str.contains(typhoon_search, case=False, na=False)
398
  if search_mask.any():
399
  for sid in typhoon_data[search_mask]['SID'].unique():
400
+ storm_data = typhoon_data[typhoon_data['SID']==sid]
401
  fig.add_trace(go.Scattergeo(
402
  lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines+markers',
403
  name=f"MATCHED: {storm_data['NAME'].iloc[0]} ({storm_data['SEASON'].iloc[0]})",
404
  line=dict(width=3, color='yellow'),
405
+ marker=dict(size=5), hoverinfo="name"
 
406
  ))
407
  fig.update_layout(
408
  title=f"Typhoon Tracks ({start_year}-{start_month} to {end_year}-{end_month})",
 
410
  projection_type='natural earth',
411
  showland=True,
412
  showcoastlines=True,
413
+ landcolor='rgb(243,243,243)',
414
+ countrycolor='rgb(204,204,204)',
415
+ coastlinecolor='rgb(204,204,204)',
416
  center=dict(lon=140, lat=20),
417
  projection_scale=3
418
  ),
 
443
  regression = perform_longitude_regression(start_year, start_month, end_year, end_month)
444
  return results[3], results[4], regression
445
 
446
+ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
447
+ if standard=='taiwan':
448
  wind_speed_ms = wind_speed * 0.514444
449
  if wind_speed_ms >= 51.0:
450
  return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['hex']
 
468
  return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
469
  return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
470
 
471
+ # ------------- Updated TSNE Cluster Function -------------
472
+ def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
473
+ try:
474
+ # Use raw typhoon data (with multiple observations per storm) merged with ONI info.
475
+ raw_data = typhoon_data.copy()
476
+ raw_data['Year'] = raw_data['ISO_TIME'].dt.year
477
+ raw_data['Month'] = raw_data['ISO_TIME'].dt.strftime('%m')
478
+ merged_raw = pd.merge(raw_data, process_oni_data(oni_data), on=['Year','Month'], how='left')
479
+
480
+ # Filter by date
481
+ start_date = datetime(start_year, start_month, 1)
482
+ end_date = datetime(end_year, end_month, 28)
483
+ merged_raw = merged_raw[(merged_raw['ISO_TIME'] >= start_date) & (merged_raw['ISO_TIME'] <= end_date)]
484
+ logging.info(f"Total points after date filtering: {merged_raw.shape[0]}")
485
+
486
+ # Filter by ENSO phase if not 'all'
487
+ merged_raw['ENSO_Phase'] = merged_raw['ONI'].apply(classify_enso_phases)
488
+ if enso_value != 'all':
489
+ merged_raw = merged_raw[merged_raw['ENSO_Phase'] == enso_value.capitalize()]
490
+ logging.info(f"Total points after ENSO filtering: {merged_raw.shape[0]}")
491
+
492
+ # Use regional filtering for Western Pacific (adjust boundaries as needed)
493
+ wp_data = merged_raw[(merged_raw['LON'] >= 100) & (merged_raw['LON'] <= 180) &
494
+ (merged_raw['LAT'] >= 0) & (merged_raw['LAT'] <= 40)]
495
+ logging.info(f"Total points after WP regional filtering: {wp_data.shape[0]}")
496
+ if wp_data.empty:
497
+ logging.info("WP regional filter returned no data; using all filtered data.")
498
+ wp_data = merged_raw
499
+
500
+ # Group by SID so each storm route has multiple observations
501
+ all_storms_data = []
502
+ for sid, group in wp_data.groupby('SID'):
503
+ group = group.sort_values('ISO_TIME')
504
+ times = pd.to_datetime(group['ISO_TIME']).values
505
+ lats = group['LAT'].astype(float).values
506
+ lons = group['LON'].astype(float).values
507
+ if len(lons) < 2:
508
+ continue
509
+ all_storms_data.append((sid, lons, lats, times))
510
+ logging.info(f"Storms available for TSNE after grouping: {len(all_storms_data)}")
511
+ if not all_storms_data:
512
+ return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms for clustering."
513
+
514
+ # Interpolate each storm's route to a common length
515
+ max_length = max(len(item[1]) for item in all_storms_data)
516
+ route_vectors = []
517
+ storm_ids = []
518
+ for sid, lons, lats, times in all_storms_data:
519
+ t = np.linspace(0, 1, len(lons))
520
+ t_new = np.linspace(0, 1, max_length)
521
+ try:
522
+ lon_interp = interp1d(t, lons, kind='linear', fill_value='extrapolate')(t_new)
523
+ lat_interp = interp1d(t, lats, kind='linear', fill_value='extrapolate')(t_new)
524
+ except Exception as ex:
525
+ logging.error(f"Interpolation error for storm {sid}: {ex}")
526
+ continue
527
+ route_vector = np.column_stack((lon_interp, lat_interp)).flatten()
528
+ if np.isnan(route_vector).any():
529
+ continue
530
+ route_vectors.append(route_vector)
531
+ storm_ids.append(sid)
532
+ logging.info(f"Storms with valid route vectors: {len(route_vectors)}")
533
+ if len(route_vectors) == 0:
534
+ return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms after interpolation."
535
+
536
+ route_vectors = np.array(route_vectors)
537
+ tsne = TSNE(n_components=2, random_state=42, verbose=1)
538
+ tsne_results = tsne.fit_transform(route_vectors)
539
+
540
+ dbscan = DBSCAN(eps=5, min_samples=3)
541
+ labels = dbscan.fit_predict(tsne_results)
542
+ unique_labels = sorted(set(labels) - {-1})
543
+
544
+ fig_tsne = go.Figure()
545
+ colors = px.colors.qualitative.Safe
546
+ for i, label in enumerate(unique_labels):
547
+ indices = np.where(labels == label)[0]
548
+ fig_tsne.add_trace(go.Scatter(
549
+ x=tsne_results[indices, 0],
550
+ y=tsne_results[indices, 1],
551
+ mode='markers',
552
+ marker=dict(color=colors[i % len(colors)]),
553
+ name=f"Cluster {label}"
554
+ ))
555
+ noise_indices = np.where(labels == -1)[0]
556
+ if len(noise_indices) > 0:
557
+ fig_tsne.add_trace(go.Scatter(
558
+ x=tsne_results[noise_indices, 0],
559
+ y=tsne_results[noise_indices, 1],
560
+ mode='markers',
561
+ marker=dict(color='grey'),
562
+ name='Noise'
563
+ ))
564
+ fig_tsne.update_layout(
565
+ title="t-SNE of Storm Routes",
566
+ xaxis_title="t-SNE Dim 1",
567
+ yaxis_title="t-SNE Dim 2"
568
+ )
569
+
570
+ # Placeholder figures for routes and stats
571
+ fig_routes = go.Figure()
572
+ fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True,
573
+ subplot_titles=("Average Wind Speed", "Average Pressure"))
574
+ info = "TSNE clustering complete."
575
+ return fig_tsne, fig_routes, fig_stats, info
576
+ except Exception as e:
577
+ logging.error(f"Error in TSNE clustering: {e}")
578
+ return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), f"Error in TSNE clustering: {e}"
579
+
580
+ # ------------- Animation Functions Using Processed CSV & Stock Map -------------
581
  def generate_track_video_from_csv(year, storm_id, standard):
582
  storm_df = typhoon_data[typhoon_data['SID'] == storm_id].copy()
583
  if storm_df.empty:
584
+ logging.error(f"No data found for storm: {storm_id}")
585
  return None
586
  storm_df = storm_df.sort_values('ISO_TIME')
587
  lats = storm_df['LAT'].astype(float).values
 
596
 
597
  min_lat, max_lat = np.min(lats), np.max(lats)
598
  min_lon, max_lon = np.min(lons), np.max(lons)
599
+ lat_padding = max((max_lat - min_lat)*0.3, 5)
600
+ lon_padding = max((max_lon - min_lon)*0.3, 5)
601
 
602
+ fig = plt.figure(figsize=(12,6), dpi=100)
603
+ ax = plt.axes([0.05, 0.05, 0.60, 0.85],
604
+ projection=ccrs.PlateCarree(central_longitude=180))
605
+ ax.stock_img()
606
  ax.set_extent([min_lon - lon_padding, max_lon + lon_padding, min_lat - lat_padding, max_lat + lat_padding],
607
  crs=ccrs.PlateCarree())
608
+ ax.coastlines(resolution='50m', color='black', linewidth=1)
609
+ gl = ax.gridlines(draw_labels=True, color='gray', alpha=0.4, linestyle='--')
610
+ gl.top_labels = gl.right_labels = False
611
+ ax.set_title(f"{year} {storm_name} - {season}", fontsize=14)
612
 
613
+ line, = ax.plot([], [], transform=ccrs.PlateCarree(), color='blue', linewidth=2)
614
+ point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
615
+ date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10,
616
+ bbox=dict(facecolor='white', alpha=0.8))
617
+ storm_info_text = fig.text(0.70, 0.60, '', fontsize=10,
618
+ bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
 
 
 
 
 
 
 
 
619
 
620
+ from matplotlib.lines import Line2D
621
+ standard_dict = atlantic_standard if standard=='atlantic' else taiwan_standard
622
+ legend_elements = [Line2D([0],[0], marker='o', color='w', label=cat,
623
+ markerfacecolor=details['hex'], markersize=8)
624
+ for cat, details in standard_dict.items()]
625
+ ax.legend(handles=legend_elements, title="Storm Categories",
626
+ loc='upper right', fontsize=9)
627
 
628
  def init():
629
  line.set_data([], [])
630
  point.set_data([], [])
631
  date_text.set_text('')
632
+ storm_info_text.set_text('')
633
+ return line, point, date_text, storm_info_text
634
 
635
  def update(frame):
636
  line.set_data(lons[:frame+1], lats[:frame+1])
 
640
  point.set_color(color)
641
  dt_str = pd.to_datetime(times[frame]).strftime('%Y-%m-%d %H:%M')
642
  date_text.set_text(dt_str)
643
+ info_str = (f"Name: {storm_name}\n"
644
+ f"Date: {dt_str}\n"
645
+ f"Wind: {wind_speed:.1f} kt\n"
646
+ f"Category: {category}")
647
+ storm_info_text.set_text(info_str)
648
+ return line, point, date_text, storm_info_text
649
 
650
  ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(times),
651
  interval=200, blit=True, repeat=True)
 
661
  storm_id = typhoon.split('(')[-1].strip(')')
662
  return generate_track_video_from_csv(year, storm_id, standard)
663
 
664
+ # ------------- Typhoon Options Update Functions -------------
665
  basin_to_prefix = {
666
  "All Basins": "all",
667
  "NA - North Atlantic": "NA",
 
680
  continue
681
  summaries.append(season_data.summary())
682
  if len(summaries) == 0:
683
+ logging.error("No storms found for given year and basin.")
684
  return gr.update(choices=[], value=None)
685
  combined_summary = pd.concat(summaries, ignore_index=True)
686
  else:
687
  prefix = basin_to_prefix.get(basin)
688
  ds = ibtracs.get(prefix)
689
  if ds is None:
690
+ logging.error(f"Dataset not found for basin {basin}")
691
  return gr.update(choices=[], value=None)
692
  season_data = ds.get_season(int(year))
693
  if season_data.summary().empty:
694
+ logging.error("No storms found for given year and basin.")
695
  return gr.update(choices=[], value=None)
696
  combined_summary = season_data.summary()
697
  options = []
 
704
  continue
705
  return gr.update(choices=options, value=options[0] if options else None)
706
  except Exception as e:
707
+ logging.error(f"Error in update_typhoon_options: {e}")
708
  return gr.update(choices=[], value=None)
709
 
710
  def update_typhoon_options_anim(year, basin):
711
  try:
 
712
  data = typhoon_data.copy()
713
+ data['Year'] = data['ISO_TIME'].dt.year
714
  season_data = data[data['Year'] == int(year)]
715
  if season_data.empty:
716
+ logging.error(f"No storms found for year {year} in animation update.")
717
  return gr.update(choices=[], value=None)
718
  summary = season_data.groupby('SID').first().reset_index()
719
  options = []
 
722
  options.append(f"{name} ({row['SID']})")
723
  return gr.update(choices=options, value=options[0] if options else None)
724
  except Exception as e:
725
+ logging.error(f"Error in update_typhoon_options_anim: {e}")
726
  return gr.update(choices=[], value=None)
727
 
728
+ # ------------- Gradio Interface -------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
730
  gr.Markdown("# Typhoon Analysis Dashboard")
731
 
732
  with gr.Tab("Overview"):
733
  gr.Markdown("""
734
  ## Welcome to the Typhoon Analysis Dashboard
735
+
736
  This dashboard allows you to analyze typhoon data in relation to ENSO phases.
737
+
738
  ### Features:
739
+ - **Track Visualization**: View typhoon tracks by time period and ENSO phase.
740
+ - **Wind Analysis**: Examine wind speed vs ONI relationships.
741
+ - **Pressure Analysis**: Analyze pressure vs ONI relationships.
742
+ - **Longitude Analysis**: Study typhoon generation longitude vs ONI.
743
+ - **Path Animation**: View animated storm tracks on a free stock world map (centered at 180°) with a dynamic sidebar and persistent legend.
744
+ - **TSNE Cluster**: Perform t-SNE clustering on WP storm routes using raw merged typhoon+ONI data with detailed error management.
 
 
745
  """)
746
 
747
  with gr.Tab("Track Visualization"):
748
  with gr.Row():
749
  start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
750
+ start_month = gr.Dropdown(label="Start Month", choices=list(range(1,13)), value=1)
751
  end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
752
+ end_month = gr.Dropdown(label="End Month", choices=list(range(1,13)), value=6)
753
+ enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all','El Nino','La Nina','Neutral'], value='all')
754
  typhoon_search = gr.Textbox(label="Typhoon Search")
755
  analyze_btn = gr.Button("Generate Tracks")
756
  tracks_plot = gr.Plot(label="Typhoon Tracks", elem_id="tracks_plot")
757
  typhoon_count = gr.Textbox(label="Number of Typhoons Displayed")
758
+ analyze_btn.click(fn=get_full_tracks,
759
+ inputs=[start_year, start_month, end_year, end_month, enso_phase, typhoon_search],
760
+ outputs=[tracks_plot, typhoon_count])
761
 
762
  with gr.Tab("Wind Analysis"):
763
  with gr.Row():
764
  wind_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
765
+ wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1,13)), value=1)
766
  wind_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
767
+ wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1,13)), value=6)
768
+ wind_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all','El Nino','La Nina','Neutral'], value='all')
769
  wind_typhoon_search = gr.Textbox(label="Typhoon Search")
770
  wind_analyze_btn = gr.Button("Generate Wind Analysis")
771
  wind_scatter = gr.Plot(label="Wind Speed vs ONI")
772
  wind_regression_results = gr.Textbox(label="Wind Regression Results")
773
+ wind_analyze_btn.click(fn=get_wind_analysis,
774
+ inputs=[wind_start_year, wind_start_month, wind_end_year, wind_end_month, wind_enso_phase, wind_typhoon_search],
775
+ outputs=[wind_scatter, wind_regression_results])
776
 
777
  with gr.Tab("Pressure Analysis"):
778
  with gr.Row():
779
  pressure_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
780
+ pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1,13)), value=1)
781
  pressure_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
782
+ pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1,13)), value=6)
783
+ pressure_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all','El Nino','La Nina','Neutral'], value='all')
784
  pressure_typhoon_search = gr.Textbox(label="Typhoon Search")
785
  pressure_analyze_btn = gr.Button("Generate Pressure Analysis")
786
  pressure_scatter = gr.Plot(label="Pressure vs ONI")
787
  pressure_regression_results = gr.Textbox(label="Pressure Regression Results")
788
+ pressure_analyze_btn.click(fn=get_pressure_analysis,
789
+ inputs=[pressure_start_year, pressure_start_month, pressure_end_year, pressure_end_month, pressure_enso_phase, pressure_typhoon_search],
790
+ outputs=[pressure_scatter, pressure_regression_results])
791
 
792
  with gr.Tab("Longitude Analysis"):
793
  with gr.Row():
794
  lon_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
795
+ lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1,13)), value=1)
796
+ lon_end_year = gr.Number(label="End Year", value=2000, minimum=1900, maximum=2024, step=1)
797
+ lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1,13)), value=6)
798
+ lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all','El Nino','La Nina','Neutral'], value='all')
799
  lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
800
  lon_analyze_btn = gr.Button("Generate Longitude Analysis")
801
  regression_plot = gr.Plot(label="Longitude vs ONI")
802
  slopes_text = gr.Textbox(label="Regression Slopes")
803
  lon_regression_results = gr.Textbox(label="Longitude Regression Results")
804
+ lon_analyze_btn.click(fn=get_longitude_analysis,
805
+ inputs=[lon_start_year, lon_start_month, lon_end_year, lon_end_month, lon_enso_phase, lon_typhoon_search],
806
+ outputs=[regression_plot, slopes_text, lon_regression_results])
807
 
808
  with gr.Tab("Tropical Cyclone Path Animation"):
809
  with gr.Row():
810
+ year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950,2025)], value="2000")
811
+ basin_dropdown = gr.Dropdown(label="Basin", choices=["NA - North Atlantic","EP - Eastern North Pacific","WP - Western North Pacific","All Basins"], value="NA - North Atlantic")
812
  with gr.Row():
813
  typhoon_dropdown = gr.Dropdown(label="Tropical Cyclone")
814
+ standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic','taiwan'], value='atlantic')
815
  animate_btn = gr.Button("Generate Animation")
816
  path_video = gr.Video(label="Tropical Cyclone Path Animation", format="mp4", interactive=False, elem_id="path_video")
817
  animation_info = gr.Markdown("""
818
  ### Animation Instructions
819
+ 1. Select a year and basin (data is from your processed CSV).
820
  2. Choose a tropical cyclone from the populated list.
821
  3. Select a classification standard (Atlantic or Taiwan).
822
  4. Click "Generate Animation".
823
+ 5. The animation displays the storm track on a free stock world map (centered at 180°) with a dynamic sidebar and a persistent legend.
824
  """)
825
  year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
826
  basin_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
827
+ animate_btn.click(fn=simplified_track_video,
828
+ inputs=[year_dropdown, basin_dropdown, typhoon_dropdown, standard_dropdown],
829
+ outputs=path_video)
830
 
831
  with gr.Tab("TSNE Cluster"):
832
  with gr.Row():
833
  tsne_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
834
+ tsne_start_month = gr.Dropdown(label="Start Month", choices=list(range(1,13)), value=1)
835
  tsne_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
836
+ tsne_end_month = gr.Dropdown(label="End Month", choices=list(range(1,13)), value=12)
837
+ tsne_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all','El Nino','La Nina','Neutral'], value='all')
838
+ tsne_season = gr.Dropdown(label="Season", choices=['all','summer','winter'], value='all')
839
  tsne_analyze_btn = gr.Button("Analyze")
840
  tsne_plot = gr.Plot(label="t-SNE Clusters")
841
  routes_plot = gr.Plot(label="Typhoon Routes with Mean Routes")
842
  stats_plot = gr.Plot(label="Cluster Statistics")
843
  cluster_info = gr.Textbox(label="Cluster Information", lines=10)
844
+ tsne_analyze_btn.click(fn=update_route_clusters,
845
+ inputs=[tsne_start_year, tsne_start_month, tsne_end_year, tsne_end_month, tsne_enso_phase, tsne_season],
846
+ outputs=[tsne_plot, routes_plot, stats_plot, cluster_info])
847
 
848
  demo.launch(share=True)