euler314 commited on
Commit
a3d5eda
·
verified ·
1 Parent(s): 55508d2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +476 -613
app.py CHANGED
@@ -107,8 +107,9 @@ class TyphoonAnalyzer:
107
  raise
108
 
109
  print("All required data files are ready")
110
-
111
  def load_initial_data(self):
 
112
  print("Loading initial data...")
113
  self.update_oni_data()
114
  self.oni_df = self.fetch_oni_data_from_csv()
@@ -120,261 +121,102 @@ class TyphoonAnalyzer:
120
  self.merged_data = self.merge_data()
121
  print("Initial data loading complete")
122
 
123
- def fetch_oni_data_from_csv(self):
124
- """Load ONI data from CSV"""
125
- df = pd.read_csv(ONI_DATA_PATH)
126
- df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
127
-
128
- # Convert month numbers to month names
129
- month_map = {
130
- '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
131
- '05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
132
- '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
133
- }
134
- df['Month'] = df['Month'].map(month_map)
135
-
136
- # Now create the date
137
- df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
138
- return df.set_index('Date')
139
-
140
- def should_update_oni(self):
141
- today = datetime.now()
142
- return (today.day == 1 or today.day == 15 or
143
- today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day)
144
  def convert_typhoondata(self, input_file, output_file):
145
- """Convert IBTrACS data to processed format"""
146
- print(f"Converting typhoon data from {input_file} to {output_file}")
147
- with open(input_file, 'r') as infile:
148
- # Skip the header lines
149
- next(infile)
150
- next(infile)
151
-
152
- reader = csv.reader(infile)
153
- sid_data = defaultdict(list)
154
-
155
- for row in reader:
156
- if not row: # Skip blank lines
157
- continue
158
-
159
- sid = row[0]
160
- iso_time = row[6]
161
- sid_data[sid].append((row, iso_time))
162
-
163
- with open(output_file, 'w', newline='') as outfile:
164
- fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME',
165
- 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES',
166
- 'START_DATE', 'END_DATE']
167
- writer = csv.DictWriter(outfile, fieldnames=fieldnames)
168
- writer.writeheader()
169
-
170
- for sid, data in sid_data.items():
171
- start_date = min(data, key=lambda x: x[1])[1]
172
- end_date = max(data, key=lambda x: x[1])[1]
173
-
174
- for row, iso_time in data:
175
- writer.writerow({
176
- 'SID': row[0],
177
- 'ISO_TIME': iso_time,
178
- 'LAT': row[8],
179
- 'LON': row[9],
180
- 'SEASON': row[1],
181
- 'NAME': row[5],
182
- 'WMO_WIND': row[10].strip() or ' ',
183
- 'WMO_PRES': row[11].strip() or ' ',
184
- 'USA_WIND': row[23].strip() or ' ',
185
- 'USA_PRES': row[24].strip() or ' ',
186
- 'START_DATE': start_date,
187
- 'END_DATE': end_date
188
- })
189
- def update_oni_data(self):
190
- if not self.should_update_oni():
191
- return
192
-
193
- url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
194
- temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
195
-
196
- try:
197
- response = requests.get(url)
198
- response.raise_for_status()
199
- with open(temp_file, 'wb') as f:
200
- f.write(response.content)
201
- self.convert_oni_ascii_to_csv(temp_file, ONI_DATA_PATH)
202
- self.last_oni_update = date.today()
203
- except Exception as e:
204
- print(f"Error updating ONI data: {e}")
205
- finally:
206
- if os.path.exists(temp_file):
207
- os.remove(temp_file)
208
- def create_wind_analysis(self, data):
209
- """Create wind speed analysis plot"""
210
- fig = px.scatter(data,
211
- x='ONI',
212
- y='USA_WIND',
213
- color='Category',
214
- color_discrete_map=COLOR_MAP,
215
- title='Wind Speed vs ONI Index',
216
- labels={
217
- 'ONI': 'Oceanic Niño Index',
218
- 'USA_WIND': 'Maximum Wind Speed (kt)'
219
- },
220
- hover_data=['NAME', 'ISO_TIME', 'Category']
221
- )
222
-
223
- # Add regression line
224
- x = data['ONI']
225
- y = data['USA_WIND']
226
- slope, intercept = np.polyfit(x, y, 1)
227
- fig.add_trace(
228
- go.Scatter(
229
- x=x,
230
- y=slope * x + intercept,
231
- mode='lines',
232
- name=f'Regression (slope={slope:.2f})',
233
- line=dict(color='black', dash='dash')
234
- )
235
- )
236
-
237
- return fig
238
- def create_typhoon_animation(self, year, typhoon_id):
239
- """Create animated visualization of typhoon path"""
240
- # Create default empty figure
241
- empty_fig = go.Figure()
242
- empty_fig.update_layout(
243
- title="No Data Available",
244
- showlegend=False,
245
- geo=dict(
246
- projection_type='mercator',
247
- showland=True,
248
- showcoastlines=True,
249
- landcolor='rgb(243, 243, 243)',
250
- countrycolor='rgb(204, 204, 204)',
251
- coastlinecolor='rgb(214, 214, 214)',
252
- showocean=True,
253
- oceancolor='rgb(230, 250, 255)',
254
- lataxis=dict(range=[0, 50]),
255
- lonaxis=dict(range=[100, 180]),
256
- center=dict(lat=20, lon=140)
257
- )
258
- )
259
-
260
- # Input validation
261
- if not typhoon_id:
262
- return empty_fig, "Please select a typhoon"
263
 
264
- # Get storm data
265
- storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
266
- if storm_data.empty:
267
- return empty_fig, "No data available for selected typhoon"
 
 
 
 
 
 
 
 
 
 
268
 
269
- storm_data = storm_data.sort_values('ISO_TIME')
270
- storm_name = storm_data['NAME'].iloc[0] if not storm_data.empty else "Unknown"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
- fig = go.Figure()
 
 
 
 
 
 
273
 
274
- # Base map settings
275
- fig.update_layout(
276
- title=f"Typhoon Path Animation - {storm_name}",
277
- showlegend=True,
278
- geo=dict(
279
- projection_type='mercator',
280
- showland=True,
281
- showcoastlines=True,
282
- landcolor='rgb(243, 243, 243)',
283
- countrycolor='rgb(204, 204, 204)',
284
- coastlinecolor='rgb(214, 214, 214)',
285
- showocean=True,
286
- oceancolor='rgb(230, 250, 255)',
287
- lataxis=dict(range=[0, 50]),
288
- lonaxis=dict(range=[100, 180]),
289
- center=dict(lat=20, lon=140)
290
- )
291
- )
292
-
293
- # Create animation frames
294
- frames = []
295
- for i in range(len(storm_data)):
296
- frame = go.Frame(
297
- data=[
298
- go.Scattergeo(
299
- lon=storm_data['LON'].iloc[:i+1],
300
- lat=storm_data['LAT'].iloc[:i+1],
301
- mode='lines+markers',
302
- line=dict(width=2, color='red'),
303
- marker=dict(size=8, color='red'),
304
- name='Path',
305
- hovertemplate=(
306
- f"Time: {storm_data['ISO_TIME'].iloc[i]:%Y-%m-%d %H:%M}<br>" +
307
- f"Wind: {storm_data['USA_WIND'].iloc[i]:.1f} kt<br>" +
308
- f"Pressure: {storm_data['WMO_PRES'].iloc[i]:.1f} hPa<br>" +
309
- f"Lat: {storm_data['LAT'].iloc[i]:.2f}°N<br>" +
310
- f"Lon: {storm_data['LON'].iloc[i]:.2f}°E"
311
- )
312
- )
313
- ],
314
- name=f'frame{i}'
315
- )
316
- frames.append(frame)
317
 
318
- fig.frames = frames
319
-
320
- # Add animation controls
321
- fig.update_layout(
322
- updatemenus=[{
323
- 'buttons': [
324
- {
325
- 'args': [None, {'frame': {'duration': 100, 'redraw': True},
326
- 'fromcurrent': True}],
327
- 'label': 'Play',
328
- 'method': 'animate'
329
- },
330
- {
331
- 'args': [[None], {'frame': {'duration': 0, 'redraw': True},
332
- 'mode': 'immediate',
333
- 'transition': {'duration': 0}}],
334
- 'label': 'Pause',
335
- 'method': 'animate'
336
- }
337
- ],
338
- 'type': 'buttons',
339
- 'showactive': False,
340
- 'x': 0.1,
341
- 'y': 0,
342
- 'xanchor': 'right',
343
- 'yanchor': 'top'
344
- }]
345
- )
346
-
347
- # Add initial data
348
- fig.add_trace(
349
- go.Scattergeo(
350
- lon=[storm_data['LON'].iloc[0]],
351
- lat=[storm_data['LAT'].iloc[0]],
352
- mode='markers',
353
- marker=dict(size=8, color='red'),
354
- name='Start Point',
355
- showlegend=True
356
- )
357
- )
358
 
359
- info_text = f"""
360
- ### Typhoon Information
361
- - Name: {storm_name}
362
- - Start Date: {storm_data['ISO_TIME'].iloc[0]:%Y-%m-%d %H:%M}
363
- - End Date: {storm_data['ISO_TIME'].iloc[-1]:%Y-%m-%d %H:%M}
364
- - Maximum Wind Speed: {storm_data['USA_WIND'].max():.1f} kt
365
- - Minimum Pressure: {storm_data['WMO_PRES'].min():.1f} hPa
366
- - Duration: {(storm_data['ISO_TIME'].iloc[-1] - storm_data['ISO_TIME'].iloc[0]).total_seconds() / 3600:.1f} hours
367
- """
368
 
369
- return fig, info_text
370
-
371
  def convert_oni_ascii_to_csv(self, input_file, output_file):
 
372
  data = defaultdict(lambda: [''] * 12)
373
  season_to_month = {
374
  'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
375
  'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
376
  }
377
-
378
  with open(input_file, 'r') as f:
379
  next(f) # Skip header
380
  for line in f:
@@ -394,6 +236,7 @@ class TyphoonAnalyzer:
394
  writer.writerow([year] + data[year])
395
 
396
  def load_ibtracs_data(self):
 
397
  if os.path.exists(CACHE_FILE):
398
  cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
399
  if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
@@ -402,24 +245,25 @@ class TyphoonAnalyzer:
402
 
403
  if os.path.exists(LOCAL_iBtrace_PATH):
404
  ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
405
- ibtracs_url=LOCAL_iBtrace_PATH)
406
  else:
407
  response = requests.get(iBtrace_uri)
408
  response.raise_for_status()
409
  with open(LOCAL_iBtrace_PATH, 'w') as f:
410
  f.write(response.text)
411
  ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
412
- ibtracs_url=LOCAL_iBtrace_PATH)
413
 
414
  with open(CACHE_FILE, 'wb') as f:
415
  pickle.dump(ibtracs, f)
416
  return ibtracs
417
 
418
  def update_typhoon_data(self):
 
419
  try:
420
  response = requests.head(iBtrace_uri)
421
  remote_modified = datetime.strptime(response.headers['Last-Modified'],
422
- '%a, %d %b %Y %H:%M:%S GMT')
423
  local_modified = (datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
424
  if os.path.exists(LOCAL_iBtrace_PATH) else datetime.min)
425
 
@@ -428,10 +272,12 @@ class TyphoonAnalyzer:
428
  response.raise_for_status()
429
  with open(LOCAL_iBtrace_PATH, 'w') as f:
430
  f.write(response.text)
 
431
  except Exception as e:
432
  print(f"Error updating typhoon data: {e}")
433
 
434
  def load_data(self):
 
435
  oni_data = pd.read_csv(ONI_DATA_PATH)
436
  typhoon_data = pd.read_csv(TYPHOON_DATA_PATH, low_memory=False)
437
  typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
@@ -439,7 +285,7 @@ class TyphoonAnalyzer:
439
 
440
  def process_oni_data(self, oni_data):
441
  """Process ONI data"""
442
- oni_long = pd.melt(oni_data, id_vars=['Year'], var_name='Month', value_name='ONI')
443
 
444
  # Create a mapping for month numbers
445
  month_map = {
@@ -454,6 +300,7 @@ class TyphoonAnalyzer:
454
  return oni_long
455
 
456
  def process_typhoon_data(self, typhoon_data):
 
457
  typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
458
  typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
459
  typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
@@ -473,9 +320,13 @@ class TyphoonAnalyzer:
473
  return typhoon_max
474
 
475
  def merge_data(self):
 
476
  return pd.merge(self.typhoon_max, self.oni_long, on=['Year', 'Month'])
477
 
478
  def categorize_typhoon(self, wind_speed):
 
 
 
479
  if wind_speed >= 137:
480
  return 'C5 Super Typhoon'
481
  elif wind_speed >= 113:
@@ -492,6 +343,7 @@ class TyphoonAnalyzer:
492
  return 'Tropical Depression'
493
 
494
  def analyze_typhoon(self, start_year, start_month, end_year, end_month, enso_value='all'):
 
495
  start_date = datetime(start_year, start_month, 1)
496
  end_date = datetime(end_year, end_month, 28)
497
 
@@ -511,7 +363,6 @@ class TyphoonAnalyzer:
511
  'tracks': self.create_tracks_plot(filtered_data),
512
  'wind': self.create_wind_analysis(filtered_data),
513
  'pressure': self.create_pressure_analysis(filtered_data),
514
- 'clusters': self.create_cluster_analysis(filtered_data, 5),
515
  'stats': self.generate_statistics(filtered_data)
516
  }
517
 
@@ -586,6 +437,96 @@ class TyphoonAnalyzer:
586
 
587
  return fig
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  def analyze_clusters(self, year, n_clusters):
590
  """Analyze typhoon clusters for a specific year"""
591
  year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
@@ -669,369 +610,321 @@ class TyphoonAnalyzer:
669
  stats_text += f"- Cluster {i+1}: {cluster_counts[i]} typhoons\n"
670
 
671
  return fig, stats_text
 
672
  def get_typhoons_for_year(self, year):
673
  """Get list of typhoons for a specific year"""
674
- year_data = self.typhoon_data[self.typhoon_data['ISO_TIME'].dt.year == year]
675
- typhoons = year_data.groupby('SID').first()
676
- return [{'label': f"{row['NAME']} ({row.name})", 'value': row.name}
677
- for _, row in typhoons.iterrows()]
 
 
 
 
 
 
 
 
 
 
678
 
679
- def create_typhoon_animation(self, year, typhoon_id):
680
  """Create animated visualization of typhoon path"""
681
- # Create default empty figure
682
- empty_fig = go.Figure()
683
- empty_fig.update_layout(
684
- title="No Data Available",
685
- showlegend=False,
 
 
 
 
 
686
  geo=dict(
687
- projection_type='mercator',
688
  showland=True,
689
- showcoastlines=True,
690
  landcolor='rgb(243, 243, 243)',
691
  countrycolor='rgb(204, 204, 204)',
692
- coastlinecolor='rgb(214, 214, 214)',
693
  showocean=True,
694
  oceancolor='rgb(230, 250, 255)',
695
  lataxis=dict(range=[0, 50]),
696
  lonaxis=dict(range=[100, 180]),
697
- center=dict(lat=20, lon=140)
698
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  )
700
 
701
- # Input validation
702
- if not typhoon_id:
703
- return empty_fig, "Please select a typhoon"
 
704
 
705
- # Get storm data
706
- try:
707
- storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
708
- if len(storm_data) == 0:
709
- return empty_fig, "No data available for selected typhoon"
 
 
 
 
710
 
711
- storm_data = storm_data.sort_values('ISO_TIME')
712
-
713
- # Get the name safely
714
- storm_name = storm_data['NAME'].values[0] if len(storm_data) > 0 else "Unknown"
715
-
716
- fig = go.Figure()
717
 
718
- # Base map settings
719
- fig.update_layout(
720
- title=f"Typhoon Path Animation - {storm_name}",
721
- showlegend=True,
722
- geo=dict(
723
- projection_type='mercator',
724
- showland=True,
725
- showcoastlines=True,
726
- landcolor='rgb(243, 243, 243)',
727
- countrycolor='rgb(204, 204, 204)',
728
- coastlinecolor='rgb(214, 214, 214)',
729
- showocean=True,
730
- oceancolor='rgb(230, 250, 255)',
731
- lataxis=dict(range=[0, 50]),
732
- lonaxis=dict(range=[100, 180]),
733
- center=dict(lat=20, lon=140)
734
- )
735
- )
736
-
737
- # Create animation frames
738
- frames = []
739
- for i in range(len(storm_data)):
740
- frame = go.Frame(
741
- data=[
742
- go.Scattergeo(
743
- lon=storm_data['LON'].values[:i+1],
744
- lat=storm_data['LAT'].values[:i+1],
745
- mode='lines+markers',
746
- line=dict(width=2, color='red'),
747
- marker=dict(size=8, color='red'),
748
- name='Path',
749
- hovertemplate=(
750
- f"Time: {pd.to_datetime(storm_data['ISO_TIME'].values[i]).strftime('%Y-%m-%d %H:%M')}<br>" +
751
- f"Wind: {storm_data['USA_WIND'].values[i]:.1f} kt<br>" +
752
- f"Pressure: {storm_data['WMO_PRES'].values[i]:.1f} hPa<br>" +
753
- f"Lat: {storm_data['LAT'].values[i]:.2f}°N<br>" +
754
- f"Lon: {storm_data['LON'].values[i]:.2f}°E"
755
- )
756
- )
757
- ],
758
- name=f'frame{i}'
759
- )
760
- frames.append(frame)
761
-
762
- fig.frames = frames
763
-
764
- # Add animation controls
765
- fig.update_layout(
766
- updatemenus=[{
767
- 'buttons': [
768
- {
769
- 'args': [None, {'frame': {'duration': 100, 'redraw': True},
770
- 'fromcurrent': True}],
771
- 'label': 'Play',
772
- 'method': 'animate'
773
- },
774
- {
775
- 'args': [[None], {'frame': {'duration': 0, 'redraw': True},
776
- 'mode': 'immediate',
777
- 'transition': {'duration': 0}}],
778
- 'label': 'Pause',
779
- 'method': 'animate'
780
- }
781
- ],
782
- 'type': 'buttons',
783
- 'showactive': False,
784
- 'x': 0.1,
785
- 'y': 0,
786
- 'xanchor': 'right',
787
- 'yanchor': 'top'
788
- }]
789
- )
790
-
791
- # Add initial data
792
- fig.add_trace(
793
- go.Scattergeo(
794
- lon=[storm_data['LON'].values[0]],
795
- lat=[storm_data['LAT'].values[0]],
796
- mode='markers',
797
- marker=dict(size=8, color='red'),
798
- name='Start Point',
799
- showlegend=True
800
- )
801
  )
 
802
 
803
- start_time = pd.to_datetime(storm_data['ISO_TIME'].values[0])
804
- end_time = pd.to_datetime(storm_data['ISO_TIME'].values[-1])
805
- duration = (end_time - start_time).total_seconds() / 3600
806
-
807
- info_text = f"""
808
- ### Typhoon Information
809
- - Name: {storm_name}
810
- - Start Date: {start_time.strftime('%Y-%m-%d %H:%M')}
811
- - End Date: {end_time.strftime('%Y-%m-%d %H:%M')}
812
- - Maximum Wind Speed: {storm_data['USA_WIND'].max():.1f} kt
813
- - Minimum Pressure: {storm_data['WMO_PRES'].min():.1f} hPa
814
- - Duration: {duration:.1f} hours
815
- """
816
-
817
- return fig, info_text
818
-
819
- except Exception as e:
820
- print(f"Error in create_typhoon_animation: {str(e)}")
821
- return empty_fig, f"Error processing typhoon data: {str(e)}"
822
-
823
- return fig, info_text
824
- def create_pressure_analysis(self, data):
825
- fig = px.scatter(data,
826
- x='ONI',
827
- y='WMO_PRES',
828
- color='Category',
829
- color_discrete_map=COLOR_MAP,
830
- title='Pressure vs ONI Index',
831
- labels={
832
- 'ONI': 'Oceanic Niño Index',
833
- 'WMO_PRES': 'Minimum Pressure (hPa)'
834
- },
835
- hover_data=['NAME', 'ISO_TIME']
836
- )
837
 
838
- # Add regression line
839
- x = data['ONI']
840
- y = data['WMO_PRES']
841
- slope, intercept = np.polyfit(x, y, 1)
842
  fig.add_trace(
843
- go.Scatter(
844
- x=x,
845
- y=slope * x + intercept,
846
  mode='lines',
847
- name=f'Regression (slope={slope:.2f})',
848
- line=dict(color='black', dash='dash')
 
849
  )
850
  )
851
-
852
- return fig
853
 
854
- def create_cluster_analysis(self, data, n_clusters=5):
855
- # Prepare data for clustering
856
- routes = []
857
- for _, storm in data.groupby('SID'):
858
- if len(storm) > 1:
859
- # Standardize route length
860
- t = np.linspace(0, 1, len(storm))
861
- t_new = np.linspace(0, 1, 100)
862
- lon_interp = interp1d(t, storm['LON'], kind='linear')(t_new)
863
- lat_interp = interp1d(t, storm['LAT'], kind='linear')(t_new)
864
- routes.append(np.column_stack((lon_interp, lat_interp)))
865
-
866
- if not routes:
867
- return go.Figure()
868
-
869
- # Perform clustering
870
- routes_array = np.array(routes)
871
- routes_reshaped = routes_array.reshape(routes_array.shape[0], -1)
872
- kmeans = KMeans(n_clusters=n_clusters, random_state=42)
873
- clusters = kmeans.fit_predict(routes_reshaped)
874
 
875
- # Create visualization
876
- fig = go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
877
 
878
- # Plot original routes colored by cluster
879
- for route, cluster_id in zip(routes, clusters):
880
- fig.add_trace(go.Scattergeo(
881
- lon=route[:, 0],
882
- lat=route[:, 1],
883
- mode='lines',
884
- line=dict(width=1, color=f'hsl({cluster_id * 360/n_clusters}, 50%, 50%)'),
885
- showlegend=False
886
- ))
887
 
888
- # Plot cluster centers
889
- for i in range(n_clusters):
890
- center = kmeans.cluster_centers_[i].reshape(-1, 2)
891
- fig.add_trace(go.Scattergeo(
892
- lon=center[:, 0],
893
- lat=center[:, 1],
894
- mode='lines',
895
- name=f'Cluster {i+1} Center',
896
- line=dict(width=3, color=f'hsl({i * 360/n_clusters}, 100%, 50%)')
897
- ))
898
 
899
- fig.update_layout(
900
- title='Typhoon Route Clusters',
901
- showlegend=True,
902
- geo=dict(
903
- projection_type='mercator',
904
- showland=True,
905
- showcoastlines=True,
906
- landcolor='rgb(243, 243, 243)',
907
- countrycolor='rgb(204, 204, 204)',
908
- coastlinecolor='rgb(214, 214, 214)',
909
- lataxis=dict(range=[0, 50]),
910
- lonaxis=dict(range=[100, 180]),
911
- )
912
- )
913
-
914
- return fig
915
- def get_typhoons_for_year(self, year):
916
- """Get list of typhoons for a specific year"""
917
- year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
918
- unique_typhoons = year_data.groupby('SID').first().reset_index()
919
- return [
920
- {'label': f"{row['NAME']} ({row['ISO_TIME'].strftime('%Y-%m-%d')})",
921
- 'value': row['SID']}
922
- for _, row in unique_typhoons.iterrows()
923
- ]
924
 
925
- def search_typhoon_details(self, year, typhoon_id):
926
- """Get detailed information for a specific typhoon"""
927
- if not typhoon_id:
928
- return None, "Please select a typhoon"
929
-
930
- storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
931
- storm_data = storm_data.sort_values('ISO_TIME')
932
 
933
- # Create track plot
934
- fig = self.create_single_typhoon_plot(storm_data)
935
 
936
- # Create detailed information text
937
- info = self.create_typhoon_info_text(storm_data)
 
938
 
939
- return fig, info
940
-
941
- def create_single_typhoon_plot(self, storm_data):
942
- """Create a detailed plot for a single typhoon"""
 
 
 
 
 
 
 
 
 
 
 
943
  fig = go.Figure()
944
 
945
  fig.update_layout(
946
- title=f"Typhoon Track - {storm_data['NAME'].iloc[0]} ({storm_data['SEASON'].iloc[0]})",
947
- showlegend=True,
948
  geo=dict(
949
- projection_type='mercator',
950
  showland=True,
951
- showcoastlines=True,
952
  landcolor='rgb(243, 243, 243)',
953
  countrycolor='rgb(204, 204, 204)',
954
- coastlinecolor='rgb(214, 214, 214)',
955
  showocean=True,
956
  oceancolor='rgb(230, 250, 255)',
957
  lataxis=dict(range=[0, 50]),
958
  lonaxis=dict(range=[100, 180]),
959
- center=dict(lat=20, lon=140)
960
  )
961
  )
962
 
963
- # Add main track
964
- fig.add_trace(go.Scattergeo(
965
- lon=storm_data['LON'],
966
- lat=storm_data['LAT'],
967
- mode='lines+markers',
968
- line=dict(width=2, color='red'),
969
- marker=dict(
970
- size=8,
971
- color=storm_data['USA_WIND'],
972
- colorscale='Viridis',
973
- showscale=True,
974
- colorbar=dict(title='Wind Speed (kt)')
975
- ),
976
- text=[f"Time: {time:%Y-%m-%d %H:%M}<br>Wind: {wind:.1f} kt<br>Pressure: {pres:.1f} hPa"
977
- for time, wind, pres in zip(storm_data['ISO_TIME'],
978
- storm_data['USA_WIND'],
979
- storm_data['WMO_PRES'])],
980
- hoverinfo='text'
981
- ))
982
 
983
- return fig
984
-
985
- def create_typhoon_info_text(self, storm_data):
986
- """Create detailed information text for a typhoon"""
987
- max_wind = storm_data['USA_WIND'].max()
988
- min_pressure = storm_data['WMO_PRES'].min()
989
- duration = (storm_data['ISO_TIME'].max() - storm_data['ISO_TIME'].min()).total_seconds() / 3600 # hours
990
-
991
- return f"""
992
- ### Typhoon Details: {storm_data['NAME'].iloc[0]}
 
 
 
 
 
 
 
993
 
994
- **Timing Information:**
995
- - Start: {storm_data['ISO_TIME'].min():%Y-%m-%d %H:%M}
996
- - End: {storm_data['ISO_TIME'].max():%Y-%m-%d %H:%M}
997
- - Duration: {duration:.1f} hours
 
 
 
 
 
 
 
 
 
998
 
999
- **Intensity Metrics:**
1000
- - Maximum Wind Speed: {max_wind:.1f} kt
1001
- - Minimum Pressure: {min_pressure:.1f} hPa
1002
- - Maximum Category: {self.categorize_typhoon(max_wind)}
1003
 
1004
- **Track Information:**
1005
- - Starting Position: {storm_data['LAT'].iloc[0]:.1f}°N, {storm_data['LON'].iloc[0]:.1f}°E
1006
- - Ending Position: {storm_data['LAT'].iloc[-1]:.1f}°N, {storm_data['LON'].iloc[-1]:.1f}°E
1007
- - Total Track Points: {len(storm_data)}
1008
- """
1009
- def generate_statistics(self, data):
1010
- stats = {
1011
- 'total_typhoons': len(data['SID'].unique()),
1012
- 'avg_wind': data['USA_WIND'].mean(),
1013
- 'max_wind': data['USA_WIND'].max(),
1014
- 'avg_pressure': data['WMO_PRES'].mean(),
1015
- 'min_pressure': data['WMO_PRES'].min(),
1016
- 'oni_correlation_wind': data['ONI'].corr(data['USA_WIND']),
1017
- 'oni_correlation_pressure': data['ONI'].corr(data['WMO_PRES']),
1018
- 'category_counts': data['Category'].value_counts().to_dict()
1019
- }
1020
 
1021
- return f"""
1022
- ### Statistical Summary
1023
-
1024
- - Total Typhoons: {stats['total_typhoons']}
1025
- - Average Wind Speed: {stats['avg_wind']:.2f} kt
1026
- - Maximum Wind Speed: {stats['max_wind']:.2f} kt
1027
- - Average Pressure: {stats['avg_pressure']:.2f} hPa
1028
- - Minimum Pressure: {stats['min_pressure']:.2f} hPa
1029
- - ONI-Wind Speed Correlation: {stats['oni_correlation_wind']:.3f}
1030
- - ONI-Pressure Correlation: {stats['oni_correlation_pressure']:.3f}
1031
 
1032
- ### Category Distribution
1033
- {chr(10).join(f'- {cat}: {count}' for cat, count in stats['category_counts'].items())}
1034
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1035
 
1036
  def create_interface():
1037
  analyzer = TyphoonAnalyzer()
@@ -1058,31 +951,21 @@ def create_interface():
1058
 
1059
  analyze_btn = gr.Button("Analyze")
1060
 
1061
- with gr.Row():
1062
- tracks_plot = gr.Plot()
1063
- with gr.Row():
1064
- wind_plot = gr.Plot()
1065
- pressure_plot = gr.Plot()
1066
 
1067
- stats_text = gr.Markdown()
1068
-
1069
- # Clustering Analysis Tab
1070
- with gr.Tab("Clustering Analysis"):
1071
  with gr.Row():
1072
- cluster_year = gr.Slider(1900, 2024, 2000, label="Year")
1073
- n_clusters = gr.Slider(2, 20, 5, label="Number of Clusters")
1074
 
1075
- cluster_btn = gr.Button("Analyze Clusters")
1076
- cluster_plot = gr.Plot()
1077
- cluster_stats = gr.Markdown()
1078
 
1079
- # Animation Tab
1080
  with gr.Tab("Typhoon Animation"):
1081
  with gr.Row():
1082
  animation_year = gr.Slider(
1083
- minimum=1900,
1084
- maximum=2024,
1085
- value=2024,
1086
  step=1,
1087
  label="Select Year"
1088
  )
@@ -1093,31 +976,27 @@ def create_interface():
1093
  label="Select Typhoon",
1094
  interactive=True
1095
  )
 
 
 
 
 
 
 
 
 
1096
 
1097
- animation_btn = gr.Button("Animate Typhoon Path", variant="primary")
1098
- animation_plot = gr.Plot()
1099
  animation_info = gr.Markdown()
1100
 
1101
  # Search Tab
1102
  with gr.Tab("Typhoon Search"):
1103
  with gr.Row():
1104
- search_year = gr.Slider(
1105
- minimum=1900,
1106
- maximum=2024,
1107
- value=2024,
1108
- step=1,
1109
- label="Select Year"
1110
- )
1111
-
1112
- with gr.Row():
1113
- search_typhoon = gr.Dropdown(
1114
- choices=[],
1115
- label="Select Typhoon",
1116
- interactive=True
1117
- )
1118
 
1119
- search_btn = gr.Button("Show Typhoon Details", variant="primary")
1120
- search_plot = gr.Plot()
1121
  search_info = gr.Markdown()
1122
 
1123
  # Event handlers
@@ -1130,9 +1009,6 @@ def create_interface():
1130
  results['stats']
1131
  ]
1132
 
1133
- def cluster_callback(year, n_clusters):
1134
- return analyzer.analyze_clusters(year, n_clusters)
1135
-
1136
  def update_typhoon_choices(year):
1137
  typhoons = analyzer.get_typhoons_for_year(year)
1138
  return gr.update(choices=typhoons, value=None)
@@ -1144,13 +1020,6 @@ def create_interface():
1144
  outputs=[tracks_plot, wind_plot, pressure_plot, stats_text]
1145
  )
1146
 
1147
- # Connect events for clustering
1148
- cluster_btn.click(
1149
- cluster_callback,
1150
- inputs=[cluster_year, n_clusters],
1151
- outputs=[cluster_plot, cluster_stats]
1152
- )
1153
-
1154
  # Connect events for Animation tab
1155
  animation_year.change(
1156
  update_typhoon_choices,
@@ -1160,21 +1029,15 @@ def create_interface():
1160
 
1161
  animation_btn.click(
1162
  analyzer.create_typhoon_animation,
1163
- inputs=[animation_year, animation_typhoon],
1164
  outputs=[animation_plot, animation_info]
1165
  )
1166
 
1167
  # Connect events for Search tab
1168
- search_year.change(
1169
- update_typhoon_choices,
1170
- inputs=[search_year],
1171
- outputs=[search_typhoon]
1172
- )
1173
-
1174
  search_btn.click(
1175
- analyzer.search_typhoon_details,
1176
- inputs=[search_year, search_typhoon],
1177
- outputs=[search_plot, search_info]
1178
  )
1179
 
1180
  return demo
 
107
  raise
108
 
109
  print("All required data files are ready")
110
+
111
  def load_initial_data(self):
112
+ """Initialize all required data"""
113
  print("Loading initial data...")
114
  self.update_oni_data()
115
  self.oni_df = self.fetch_oni_data_from_csv()
 
121
  self.merged_data = self.merge_data()
122
  print("Initial data loading complete")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def convert_typhoondata(self, input_file, output_file):
125
+ """Convert IBTrACS data to processed format"""
126
+ print(f"Converting typhoon data from {input_file} to {output_file}")
127
+ with open(input_file, 'r') as infile:
128
+ # Skip the header lines
129
+ next(infile)
130
+ next(infile)
131
+
132
+ reader = csv.reader(infile)
133
+ sid_data = defaultdict(list)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ for row in reader:
136
+ if not row: # Skip blank lines
137
+ continue
138
+
139
+ sid = row[0]
140
+ iso_time = row[6]
141
+ sid_data[sid].append((row, iso_time))
142
+
143
+ with open(output_file, 'w', newline='') as outfile:
144
+ fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME',
145
+ 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES',
146
+ 'START_DATE', 'END_DATE']
147
+ writer = csv.DictWriter(outfile, fieldnames=fieldnames)
148
+ writer.writeheader()
149
 
150
+ for sid, data in sid_data.items():
151
+ start_date = min(data, key=lambda x: x[1])[1]
152
+ end_date = max(data, key=lambda x: x[1])[1]
153
+
154
+ for row, iso_time in data:
155
+ writer.writerow({
156
+ 'SID': row[0],
157
+ 'ISO_TIME': iso_time,
158
+ 'LAT': row[8],
159
+ 'LON': row[9],
160
+ 'SEASON': row[1],
161
+ 'NAME': row[5],
162
+ 'WMO_WIND': row[10].strip() or ' ',
163
+ 'WMO_PRES': row[11].strip() or ' ',
164
+ 'USA_WIND': row[23].strip() or ' ',
165
+ 'USA_PRES': row[24].strip() or ' ',
166
+ 'START_DATE': start_date,
167
+ 'END_DATE': end_date
168
+ })
169
+
170
+ def fetch_oni_data_from_csv(self):
171
+ """Load ONI data from CSV"""
172
+ df = pd.read_csv(ONI_DATA_PATH)
173
+ df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
174
 
175
+ # Convert month numbers to month names
176
+ month_map = {
177
+ '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
178
+ '05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
179
+ '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
180
+ }
181
+ df['Month'] = df['Month'].map(month_map)
182
 
183
+ # Now create the date
184
+ df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
185
+ return df.set_index('Date')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
+ def update_oni_data(self):
188
+ """Update ONI data from NOAA"""
189
+ if not self._should_update_oni():
190
+ return
191
+
192
+ url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
193
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
194
+ try:
195
+ response = requests.get(url)
196
+ response.raise_for_status()
197
+ temp_file.write(response.content)
198
+ self.convert_oni_ascii_to_csv(temp_file.name, ONI_DATA_PATH)
199
+ self.last_oni_update = date.today()
200
+ except Exception as e:
201
+ print(f"Error updating ONI data: {e}")
202
+ finally:
203
+ if os.path.exists(temp_file.name):
204
+ os.remove(temp_file.name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
+ def _should_update_oni(self):
207
+ """Check if ONI data should be updated"""
208
+ today = datetime.now()
209
+ return (today.day in [1, 15] or
210
+ today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day)
 
 
 
 
211
 
 
 
212
  def convert_oni_ascii_to_csv(self, input_file, output_file):
213
+ """Convert ONI ASCII data to CSV format"""
214
  data = defaultdict(lambda: [''] * 12)
215
  season_to_month = {
216
  'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
217
  'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
218
  }
219
+
220
  with open(input_file, 'r') as f:
221
  next(f) # Skip header
222
  for line in f:
 
236
  writer.writerow([year] + data[year])
237
 
238
  def load_ibtracs_data(self):
239
+ """Load IBTrACS data with caching"""
240
  if os.path.exists(CACHE_FILE):
241
  cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
242
  if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
 
245
 
246
  if os.path.exists(LOCAL_iBtrace_PATH):
247
  ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
248
+ ibtracs_url=LOCAL_iBtrace_PATH)
249
  else:
250
  response = requests.get(iBtrace_uri)
251
  response.raise_for_status()
252
  with open(LOCAL_iBtrace_PATH, 'w') as f:
253
  f.write(response.text)
254
  ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
255
+ ibtracs_url=LOCAL_iBtrace_PATH)
256
 
257
  with open(CACHE_FILE, 'wb') as f:
258
  pickle.dump(ibtracs, f)
259
  return ibtracs
260
 
261
  def update_typhoon_data(self):
262
+ """Update typhoon data from IBTrACS"""
263
  try:
264
  response = requests.head(iBtrace_uri)
265
  remote_modified = datetime.strptime(response.headers['Last-Modified'],
266
+ '%a, %d %b %Y %H:%M:%S GMT')
267
  local_modified = (datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
268
  if os.path.exists(LOCAL_iBtrace_PATH) else datetime.min)
269
 
 
272
  response.raise_for_status()
273
  with open(LOCAL_iBtrace_PATH, 'w') as f:
274
  f.write(response.text)
275
+ print("Typhoon data updated successfully")
276
  except Exception as e:
277
  print(f"Error updating typhoon data: {e}")
278
 
279
  def load_data(self):
280
+ """Load ONI and typhoon data"""
281
  oni_data = pd.read_csv(ONI_DATA_PATH)
282
  typhoon_data = pd.read_csv(TYPHOON_DATA_PATH, low_memory=False)
283
  typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
 
285
 
286
  def process_oni_data(self, oni_data):
287
  """Process ONI data"""
288
+ oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
289
 
290
  # Create a mapping for month numbers
291
  month_map = {
 
300
  return oni_long
301
 
302
  def process_typhoon_data(self, typhoon_data):
303
+ """Process typhoon data"""
304
  typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
305
  typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
306
  typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
 
320
  return typhoon_max
321
 
322
  def merge_data(self):
323
+ """Merge ONI and typhoon data"""
324
  return pd.merge(self.typhoon_max, self.oni_long, on=['Year', 'Month'])
325
 
326
  def categorize_typhoon(self, wind_speed):
327
+ """Categorize typhoon based on wind speed"""
328
+ if np.isnan(wind_speed):
329
+ return 'Unknown'
330
  if wind_speed >= 137:
331
  return 'C5 Super Typhoon'
332
  elif wind_speed >= 113:
 
343
  return 'Tropical Depression'
344
 
345
  def analyze_typhoon(self, start_year, start_month, end_year, end_month, enso_value='all'):
346
+ """Main analysis function"""
347
  start_date = datetime(start_year, start_month, 1)
348
  end_date = datetime(end_year, end_month, 28)
349
 
 
363
  'tracks': self.create_tracks_plot(filtered_data),
364
  'wind': self.create_wind_analysis(filtered_data),
365
  'pressure': self.create_pressure_analysis(filtered_data),
 
366
  'stats': self.generate_statistics(filtered_data)
367
  }
368
 
 
437
 
438
  return fig
439
 
440
+ def create_wind_analysis(self, data):
441
+ """Create wind speed analysis plot"""
442
+ fig = px.scatter(data,
443
+ x='ONI',
444
+ y='USA_WIND',
445
+ color='Category',
446
+ color_discrete_map=COLOR_MAP,
447
+ title='Wind Speed vs ONI Index',
448
+ labels={
449
+ 'ONI': 'Oceanic Niño Index',
450
+ 'USA_WIND': 'Maximum Wind Speed (kt)'
451
+ },
452
+ hover_data=['NAME', 'ISO_TIME', 'Category']
453
+ )
454
+
455
+ # Add regression line
456
+ x = data['ONI']
457
+ y = data['USA_WIND']
458
+ slope, intercept = np.polyfit(x, y, 1)
459
+ fig.add_trace(
460
+ go.Scatter(
461
+ x=x,
462
+ y=slope * x + intercept,
463
+ mode='lines',
464
+ name=f'Regression (slope={slope:.2f})',
465
+ line=dict(color='black', dash='dash')
466
+ )
467
+ )
468
+
469
+ return fig
470
+
471
+ def create_pressure_analysis(self, data):
472
+ """Create pressure analysis plot"""
473
+ fig = px.scatter(data,
474
+ x='ONI',
475
+ y='WMO_PRES',
476
+ color='Category',
477
+ color_discrete_map=COLOR_MAP,
478
+ title='Pressure vs ONI Index',
479
+ labels={
480
+ 'ONI': 'Oceanic Niño Index',
481
+ 'WMO_PRES': 'Minimum Pressure (hPa)'
482
+ },
483
+ hover_data=['NAME', 'ISO_TIME', 'Category']
484
+ )
485
+
486
+ # Add regression line
487
+ x = data['ONI']
488
+ y = data['WMO_PRES']
489
+ slope, intercept = np.polyfit(x, y, 1)
490
+ fig.add_trace(
491
+ go.Scatter(
492
+ x=x,
493
+ y=slope * x + intercept,
494
+ mode='lines',
495
+ name=f'Regression (slope={slope:.2f})',
496
+ line=dict(color='black', dash='dash')
497
+ )
498
+ )
499
+
500
+ return fig
501
+
502
+ def generate_statistics(self, data):
503
+ """Generate statistical summary"""
504
+ stats = {
505
+ 'total_typhoons': len(data['SID'].unique()),
506
+ 'avg_wind': data['USA_WIND'].mean(),
507
+ 'max_wind': data['USA_WIND'].max(),
508
+ 'avg_pressure': data['WMO_PRES'].mean(),
509
+ 'min_pressure': data['WMO_PRES'].min(),
510
+ 'oni_correlation_wind': data['ONI'].corr(data['USA_WIND']),
511
+ 'oni_correlation_pressure': data['ONI'].corr(data['WMO_PRES']),
512
+ 'category_counts': data['Category'].value_counts().to_dict()
513
+ }
514
+
515
+ return f"""
516
+ ### Statistical Summary
517
+
518
+ - Total Typhoons: {stats['total_typhoons']}
519
+ - Average Wind Speed: {stats['avg_wind']:.2f} kt
520
+ - Maximum Wind Speed: {stats['max_wind']:.2f} kt
521
+ - Average Pressure: {stats['avg_pressure']:.2f} hPa
522
+ - Minimum Pressure: {stats['min_pressure']:.2f} hPa
523
+ - ONI-Wind Speed Correlation: {stats['oni_correlation_wind']:.3f}
524
+ - ONI-Pressure Correlation: {stats['oni_correlation_pressure']:.3f}
525
+
526
+ ### Category Distribution
527
+ {chr(10).join(f'- {cat}: {count}' for cat, count in stats['category_counts'].items())}
528
+ """
529
+
530
  def analyze_clusters(self, year, n_clusters):
531
  """Analyze typhoon clusters for a specific year"""
532
  year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
 
610
  stats_text += f"- Cluster {i+1}: {cluster_counts[i]} typhoons\n"
611
 
612
  return fig, stats_text
613
+
614
  def get_typhoons_for_year(self, year):
615
  """Get list of typhoons for a specific year"""
616
+ try:
617
+ season = self.ibtracs.get_season(year)
618
+ storm_summary = season.summary()
619
+
620
+ typhoon_options = []
621
+ for i in range(storm_summary['season_storms']):
622
+ storm_id = storm_summary['id'][i]
623
+ storm_name = storm_summary['name'][i]
624
+ typhoon_options.append({'label': f"{storm_name} ({storm_id})", 'value': storm_id})
625
+
626
+ return typhoon_options
627
+ except Exception as e:
628
+ print(f"Error getting typhoons for year {year}: {str(e)}")
629
+ return []
630
 
631
+ def create_typhoon_animation(self, year, storm_id, standard='atlantic'):
632
  """Create animated visualization of typhoon path"""
633
+ if not storm_id:
634
+ return go.Figure(), "Please select a typhoon"
635
+
636
+ storm = self.ibtracs.get_storm(storm_id)
637
+
638
+ fig = go.Figure()
639
+
640
+ # Base map setup with correct scaling
641
+ fig.update_layout(
642
+ title=f"{year} - {storm.name} Typhoon Path",
643
  geo=dict(
644
+ projection_type='natural earth',
645
  showland=True,
 
646
  landcolor='rgb(243, 243, 243)',
647
  countrycolor='rgb(204, 204, 204)',
648
+ coastlinecolor='rgb(100, 100, 100)',
649
  showocean=True,
650
  oceancolor='rgb(230, 250, 255)',
651
  lataxis=dict(range=[0, 50]),
652
  lonaxis=dict(range=[100, 180]),
653
+ center=dict(lat=20, lon=140),
654
+ ),
655
+ updatemenus=[{
656
+ "buttons": [
657
+ {
658
+ "args": [None, {"frame": {"duration": 100, "redraw": True},
659
+ "fromcurrent": True,
660
+ "transition": {"duration": 0}}],
661
+ "label": "Play",
662
+ "method": "animate"
663
+ },
664
+ {
665
+ "args": [[None], {"frame": {"duration": 0, "redraw": True},
666
+ "mode": "immediate",
667
+ "transition": {"duration": 0}}],
668
+ "label": "Pause",
669
+ "method": "animate"
670
+ }
671
+ ],
672
+ "direction": "left",
673
+ "pad": {"r": 10, "t": 87},
674
+ "showactive": False,
675
+ "type": "buttons",
676
+ "x": 0.1,
677
+ "xanchor": "right",
678
+ "y": 0,
679
+ "yanchor": "top"
680
+ }]
681
  )
682
 
683
+ # Create animation frames
684
+ frames = []
685
+ for i in range(len(storm.time)):
686
+ category, color = self.categorize_typhoon_by_standard(storm.vmax[i], standard)
687
 
688
+ # Get extra radius data if available
689
+ radius_info = ""
690
+ if hasattr(storm, 'dict'):
691
+ r34_ne = storm.dict.get('USA_R34_NE', [None])[i] if 'USA_R34_NE' in storm.dict else None
692
+ r34_se = storm.dict.get('USA_R34_SE', [None])[i] if 'USA_R34_SE' in storm.dict else None
693
+ r34_sw = storm.dict.get('USA_R34_SW', [None])[i] if 'USA_R34_SW' in storm.dict else None
694
+ r34_nw = storm.dict.get('USA_R34_NW', [None])[i] if 'USA_R34_NW' in storm.dict else None
695
+ rmw = storm.dict.get('USA_RMW', [None])[i] if 'USA_RMW' in storm.dict else None
696
+ eye = storm.dict.get('USA_EYE', [None])[i] if 'USA_EYE' in storm.dict else None
697
 
698
+ if any([r34_ne, r34_se, r34_sw, r34_nw, rmw, eye]):
699
+ radius_info = f"<br>R34: NE={r34_ne}, SE={r34_se}, SW={r34_sw}, NW={r34_nw}<br>"
700
+ radius_info += f"RMW: {rmw}<br>Eye Diameter: {eye}"
 
 
 
701
 
702
+ frame = go.Frame(
703
+ data=[
704
+ go.Scattergeo(
705
+ lon=storm.lon[:i+1],
706
+ lat=storm.lat[:i+1],
707
+ mode='lines',
708
+ line=dict(width=2, color='blue'),
709
+ name='Path Traveled',
710
+ showlegend=False,
711
+ ),
712
+ go.Scattergeo(
713
+ lon=[storm.lon[i]],
714
+ lat=[storm.lat[i]],
715
+ mode='markers+text',
716
+ marker=dict(size=10, color=color, symbol='star'),
717
+ text=category,
718
+ textposition="top center",
719
+ textfont=dict(size=12, color=color),
720
+ name='Current Location',
721
+ hovertemplate=(
722
+ f"{storm.time[i].strftime('%Y-%m-%d %H:%M')}<br>"
723
+ f"Category: {category}<br>"
724
+ f"Wind Speed: {storm.vmax[i]:.1f} kt<br>"
725
+ f"{radius_info}"
726
+ ),
727
+ ),
728
+ ],name=f"frame{i}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  )
730
+ frames.append(frame)
731
 
732
+ fig.frames = frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
 
734
+ # Add initial track and starting point
 
 
 
735
  fig.add_trace(
736
+ go.Scattergeo(
737
+ lon=storm.lon,
738
+ lat=storm.lat,
739
  mode='lines',
740
+ line=dict(width=2, color='gray'),
741
+ name='Complete Path',
742
+ showlegend=True,
743
  )
744
  )
 
 
745
 
746
+ fig.add_trace(
747
+ go.Scattergeo(
748
+ lon=[storm.lon[0]],
749
+ lat=[storm.lat[0]],
750
+ mode='markers',
751
+ marker=dict(size=10, color='green', symbol='star'),
752
+ name='Starting Point',
753
+ text=storm.time[0].strftime('%Y-%m-%d %H:%M'),
754
+ hoverinfo='text+name',
755
+ )
756
+ )
 
 
 
 
 
 
 
 
 
757
 
758
+ # Add slider for frame selection
759
+ sliders = [{
760
+ "active": 0,
761
+ "yanchor": "top",
762
+ "xanchor": "left",
763
+ "currentvalue": {
764
+ "font": {"size": 20},
765
+ "prefix": "Time: ",
766
+ "visible": True,
767
+ "xanchor": "right"
768
+ },
769
+ "transition": {"duration": 100, "easing": "cubic-in-out"},
770
+ "pad": {"b": 10, "t": 50},
771
+ "len": 0.9,
772
+ "x": 0.1,
773
+ "y": 0,
774
+ "steps": [
775
+ {
776
+ "args": [[f"frame{k}"],
777
+ {"frame": {"duration": 100, "redraw": True},
778
+ "mode": "immediate",
779
+ "transition": {"duration": 0}}
780
+ ],
781
+ "label": storm.time[k].strftime('%Y-%m-%d %H:%M'),
782
+ "method": "animate"
783
+ }
784
+ for k in range(len(storm.time))
785
+ ]
786
+ }]
787
 
788
+ fig.update_layout(sliders=sliders)
 
 
 
 
 
 
 
 
789
 
790
+ info_text = f"""
791
+ ### Typhoon Information
792
+ - **Name:** {storm.name}
793
+ - **Start Date:** {storm.time[0].strftime('%Y-%m-%d %H:%M')}
794
+ - **End Date:** {storm.time[-1].strftime('%Y-%m-%d %H:%M')}
795
+ - **Duration:** {(storm.time[-1] - storm.time[0]).total_seconds() / 3600:.1f} hours
796
+ - **Maximum Wind Speed:** {max(storm.vmax):.1f} kt
797
+ - **Minimum Pressure:** {min(storm.mslp):.1f} hPa
798
+ - **Peak Category:** {self.categorize_typhoon_by_standard(max(storm.vmax), standard)[0]}
799
+ """
800
 
801
+ return fig, info_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
 
803
+ def search_typhoons(self, query):
804
+ """Search for typhoons by name"""
805
+ if not query:
806
+ return go.Figure(), "Please enter a typhoon name to search"
 
 
 
807
 
808
+ # Find all typhoons matching the query
809
+ matching_storms = []
810
 
811
+ # Limit search to last 30 years to improve performance
812
+ current_year = datetime.now().year
813
+ start_year = current_year - 30
814
 
815
+ for year in range(start_year, current_year + 1):
816
+ try:
817
+ season = self.ibtracs.get_season(year)
818
+ for storm_id in season.summary()['id']:
819
+ storm = self.ibtracs.get_storm(storm_id)
820
+ if query.lower() in storm.name.lower():
821
+ matching_storms.append((year, storm))
822
+ except Exception as e:
823
+ print(f"Error searching year {year}: {str(e)}")
824
+ continue
825
+
826
+ if not matching_storms:
827
+ return go.Figure(), "No typhoons found matching your search"
828
+
829
+ # Create visualization of all matching typhoons
830
  fig = go.Figure()
831
 
832
  fig.update_layout(
833
+ title=f"Typhoons Matching: '{query}'",
 
834
  geo=dict(
835
+ projection_type='natural earth',
836
  showland=True,
 
837
  landcolor='rgb(243, 243, 243)',
838
  countrycolor='rgb(204, 204, 204)',
839
+ coastlinecolor='rgb(100, 100, 100)',
840
  showocean=True,
841
  oceancolor='rgb(230, 250, 255)',
842
  lataxis=dict(range=[0, 50]),
843
  lonaxis=dict(range=[100, 180]),
844
+ center=dict(lat=20, lon=140),
845
  )
846
  )
847
 
848
+ # Plot each matching storm with a different color
849
+ colors = px.colors.qualitative.Plotly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
 
851
+ for i, (year, storm) in enumerate(matching_storms):
852
+ color = colors[i % len(colors)]
853
+
854
+ fig.add_trace(go.Scattergeo(
855
+ lon=storm.lon,
856
+ lat=storm.lat,
857
+ mode='lines',
858
+ line=dict(width=3, color=color),
859
+ name=f"{storm.name} ({year})",
860
+ hovertemplate=(
861
+ f"Name: {storm.name}<br>"
862
+ f"Year: {year}<br>"
863
+ f"Max Wind: {max(storm.vmax):.1f} kt<br>"
864
+ f"Min Pressure: {min(storm.mslp):.1f} hPa<br>"
865
+ f"Position: %{lat:.2f}°N, %{lon:.2f}°E"
866
+ )
867
+ ))
868
 
869
+ # Add starting points
870
+ for i, (year, storm) in enumerate(matching_storms):
871
+ color = colors[i % len(colors)]
872
+
873
+ fig.add_trace(go.Scattergeo(
874
+ lon=[storm.lon[0]],
875
+ lat=[storm.lat[0]],
876
+ mode='markers',
877
+ marker=dict(size=10, color=color, symbol='circle'),
878
+ name=f"Start: {storm.name} ({year})",
879
+ showlegend=False,
880
+ hoverinfo='name'
881
+ ))
882
 
883
+ # Create information text
884
+ info_text = f"### Found {len(matching_storms)} typhoons matching '{query}':\n\n"
 
 
885
 
886
+ for year, storm in matching_storms:
887
+ info_text += f"- **{storm.name} ({year})**\n"
888
+ info_text += f" - Time: {storm.time[0].strftime('%Y-%m-%d')} to {storm.time[-1].strftime('%Y-%m-%d')}\n"
889
+ info_text += f" - Max Wind: {max(storm.vmax):.1f} kt\n"
890
+ info_text += f" - Min Pressure: {min(storm.mslp):.1f} hPa\n"
891
+ info_text += f" - Category: {self.categorize_typhoon_by_standard(max(storm.vmax))[0]}\n\n"
 
 
 
 
 
 
 
 
 
 
892
 
893
+ return fig, info_text
 
 
 
 
 
 
 
 
 
894
 
895
+ def categorize_typhoon_by_standard(self, wind_speed, standard='atlantic'):
896
+ """
897
+ Categorize typhoon based on wind speed and chosen standard
898
+ wind_speed is in knots
899
+ """
900
+ if standard == 'taiwan':
901
+ # Convert knots to m/s for Taiwan standard
902
+ wind_speed_ms = wind_speed * 0.514444
903
+
904
+ if wind_speed_ms >= 51.0:
905
+ return 'Strong Typhoon', 'rgb(255, 0, 0)'
906
+ elif wind_speed_ms >= 33.7:
907
+ return 'Medium Typhoon', 'rgb(255, 127, 0)'
908
+ elif wind_speed_ms >= 17.2:
909
+ return 'Mild Typhoon', 'rgb(255, 255, 0)'
910
+ else:
911
+ return 'Tropical Depression', 'rgb(173, 216, 230)'
912
+ else:
913
+ # Atlantic standard uses knots
914
+ if wind_speed >= 137:
915
+ return 'C5 Super Typhoon', 'rgb(255, 0, 0)'
916
+ elif wind_speed >= 113:
917
+ return 'C4 Very Strong Typhoon', 'rgb(255, 63, 0)'
918
+ elif wind_speed >= 96:
919
+ return 'C3 Strong Typhoon', 'rgb(255, 127, 0)'
920
+ elif wind_speed >= 83:
921
+ return 'C2 Typhoon', 'rgb(255, 191, 0)'
922
+ elif wind_speed >= 64:
923
+ return 'C1 Typhoon', 'rgb(255, 255, 0)'
924
+ elif wind_speed >= 34:
925
+ return 'Tropical Storm', 'rgb(0, 255, 255)'
926
+ else:
927
+ return 'Tropical Depression', 'rgb(173, 216, 230)'
928
 
929
  def create_interface():
930
  analyzer = TyphoonAnalyzer()
 
951
 
952
  analyze_btn = gr.Button("Analyze")
953
 
954
+ tracks_plot = gr.Plot(label="Typhoon Tracks")
 
 
 
 
955
 
 
 
 
 
956
  with gr.Row():
957
+ wind_plot = gr.Plot(label="Wind Speed Analysis")
958
+ pressure_plot = gr.Plot(label="Pressure Analysis")
959
 
960
+ stats_text = gr.Markdown()
 
 
961
 
962
+ # Typhoon Animation Tab
963
  with gr.Tab("Typhoon Animation"):
964
  with gr.Row():
965
  animation_year = gr.Slider(
966
+ minimum=1950,
967
+ maximum=2024,
968
+ value=2020,
969
  step=1,
970
  label="Select Year"
971
  )
 
976
  label="Select Typhoon",
977
  interactive=True
978
  )
979
+
980
+ standard_dropdown = gr.Dropdown(
981
+ choices=[
982
+ {"label": "Atlantic Standard", "value": "atlantic"},
983
+ {"label": "Taiwan Standard", "value": "taiwan"}
984
+ ],
985
+ value="atlantic",
986
+ label="Classification Standard"
987
+ )
988
 
989
+ animation_btn = gr.Button("Show Typhoon Path", variant="primary")
990
+ animation_plot = gr.Plot(label="Typhoon Path Animation")
991
  animation_info = gr.Markdown()
992
 
993
  # Search Tab
994
  with gr.Tab("Typhoon Search"):
995
  with gr.Row():
996
+ search_input = gr.Textbox(label="Search Typhoon Name")
997
+ search_btn = gr.Button("Search Typhoons", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
998
 
999
+ search_results = gr.Plot(label="Search Results")
 
1000
  search_info = gr.Markdown()
1001
 
1002
  # Event handlers
 
1009
  results['stats']
1010
  ]
1011
 
 
 
 
1012
  def update_typhoon_choices(year):
1013
  typhoons = analyzer.get_typhoons_for_year(year)
1014
  return gr.update(choices=typhoons, value=None)
 
1020
  outputs=[tracks_plot, wind_plot, pressure_plot, stats_text]
1021
  )
1022
 
 
 
 
 
 
 
 
1023
  # Connect events for Animation tab
1024
  animation_year.change(
1025
  update_typhoon_choices,
 
1029
 
1030
  animation_btn.click(
1031
  analyzer.create_typhoon_animation,
1032
+ inputs=[animation_year, animation_typhoon, standard_dropdown],
1033
  outputs=[animation_plot, animation_info]
1034
  )
1035
 
1036
  # Connect events for Search tab
 
 
 
 
 
 
1037
  search_btn.click(
1038
+ analyzer.search_typhoons,
1039
+ inputs=[search_input],
1040
+ outputs=[search_results, search_info]
1041
  )
1042
 
1043
  return demo