euler314 commited on
Commit
b7a2a27
·
verified ·
1 Parent(s): b1d12e5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -24
app.py CHANGED
@@ -130,14 +130,16 @@ def load_ibtracs_data():
130
  with open(CACHE_FILE, 'rb') as f:
131
  return pickle.load(f)
132
  if os.path.exists(LOCAL_iBtrace_PATH):
133
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
 
134
  else:
135
  response = requests.get(iBtrace_uri)
136
  response.raise_for_status()
137
  with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
138
  temp_file.write(response.text)
139
  shutil.move(temp_file.name, LOCAL_iBtrace_PATH)
140
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
 
141
  with open(CACHE_FILE, 'wb') as f:
142
  pickle.dump(ibtracs, f)
143
  return ibtracs
@@ -323,7 +325,7 @@ def generate_main_analysis(start_year, start_month, end_year, end_month, enso_ph
323
 
324
  return tracks_fig, wind_scatter, pressure_scatter, regression_fig, slopes_text
325
 
326
- # Video animation function with fixed sidebar
327
  def categorize_typhoon_by_standard(wind_speed, standard):
328
  if standard == 'taiwan':
329
  wind_speed_ms = wind_speed * 0.514444
@@ -349,7 +351,7 @@ def categorize_typhoon_by_standard(wind_speed, standard):
349
  return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
350
  return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
351
 
352
- def generate_track_video(year, typhoon, standard):
353
  if not typhoon:
354
  return None
355
 
@@ -374,15 +376,21 @@ def generate_track_video(year, typhoon, standard):
374
  ax.add_feature(cfeature.BORDERS, linestyle=':', edgecolor='gray')
375
  ax.gridlines(draw_labels=True, linestyle='--', color='gray', alpha=0.5)
376
 
377
- ax.set_title(f"{year} {storm.name} Typhoon Path")
378
 
379
  # Initialize the line and point
380
  line, = ax.plot([], [], 'b-', linewidth=2, transform=ccrs.PlateCarree())
381
  point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
382
  date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
383
 
 
 
 
 
 
 
384
  # Add sidebar on the right with adjusted positions
385
- details_title = fig.text(0.7, 0.95, "Typhoon Details", fontsize=12, fontweight='bold', verticalalignment='top')
386
  details_text = fig.text(0.7, 0.85, '', fontsize=12, verticalalignment='top',
387
  bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
388
 
@@ -394,12 +402,25 @@ def generate_track_video(year, typhoon, standard):
394
  fig.legend(handles=legend_elements, title="Color Legend", loc='center right',
395
  bbox_to_anchor=(0.95, 0.5), fontsize=10)
396
 
 
 
 
 
 
 
 
 
 
397
  def init():
398
  line.set_data([], [])
399
  point.set_data([], [])
400
  date_text.set_text('')
401
  details_text.set_text('')
402
- return line, point, date_text, details_text
 
 
 
 
403
 
404
  def update(frame):
405
  line.set_data(storm.lon[:frame+1], storm.lat[:frame+1])
@@ -407,12 +428,70 @@ def generate_track_video(year, typhoon, standard):
407
  point.set_data([storm.lon[frame]], [storm.lat[frame]])
408
  point.set_color(color)
409
  date_text.set_text(storm.time[frame].strftime('%Y-%m-%d %H:%M'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  details = f"Name: {storm.name}\n" \
411
  f"Date: {storm.time[frame].strftime('%Y-%m-%d %H:%M')}\n" \
412
  f"Wind Speed: {storm.vmax[frame]:.1f} kt\n" \
413
- f"Category: {category}"
 
 
 
414
  details_text.set_text(details)
415
- return line, point, date_text, details_text
416
 
417
  ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(storm.time),
418
  interval=200, blit=True, repeat=True)
@@ -833,7 +912,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
833
  - **Wind Analysis**: Examine wind speed vs ONI relationships
834
  - **Pressure Analysis**: Analyze pressure vs ONI relationships
835
  - **Longitude Analysis**: Study typhoon generation longitude vs ONI
836
- - **Path Animation**: Watch animated typhoon paths with a sidebar
837
  - **TSNE Cluster**: Perform t-SNE clustering on typhoon routes with mean routes and region analysis
838
 
839
  Select a tab above to begin your analysis.
@@ -990,33 +1069,47 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
990
  with gr.Tab("Typhoon Path Animation"):
991
  with gr.Row():
992
  year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950, 2025)], value="2024")
993
- typhoon_dropdown = gr.Dropdown(label="Typhoon")
 
994
  standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
995
 
996
  animate_btn = gr.Button("Generate Animation")
997
- path_video = gr.Video(label="Typhoon Path Animation", elem_id="path_video")
998
  animation_info = gr.Markdown("""
999
  ### Animation Instructions
1000
- 1. Select a year and typhoon from the dropdowns
1001
- 2. Choose a classification standard (Atlantic or Taiwan)
1002
- 3. Click "Generate Animation"
1003
- 4. Use the video player's built-in controls to play, pause, or scrub through the animation
1004
- 5. The animation shows the typhoon track growing over a world map, with:
1005
- - Date on the bottom left
1006
- - Sidebar on the right showing typhoon details (name, date, wind speed, category) as it moves
1007
- - Color legend with colored markers centered on the right
 
 
 
 
 
1008
  """)
1009
 
1010
- def update_typhoon_options(year):
1011
  season = ibtracs.get_season(int(year))
1012
  storm_summary = season.summary()
1013
- options = [f"{storm_summary['name'][i]} ({storm_summary['id'][i]})" for i in range(storm_summary['season_storms'])]
 
 
1014
  return gr.update(choices=options, value=options[0] if options else None)
1015
 
1016
- year_dropdown.change(fn=update_typhoon_options, inputs=year_dropdown, outputs=typhoon_dropdown)
 
 
 
 
 
 
1017
  animate_btn.click(
1018
  fn=generate_track_video,
1019
- inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
1020
  outputs=path_video
1021
  )
1022
 
 
130
  with open(CACHE_FILE, 'rb') as f:
131
  return pickle.load(f)
132
  if os.path.exists(LOCAL_iBtrace_PATH):
133
+ # Remove the basin='west_pacific' parameter to load all basins
134
+ ibtracs = tracks.TrackDataset(source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
135
  else:
136
  response = requests.get(iBtrace_uri)
137
  response.raise_for_status()
138
  with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
139
  temp_file.write(response.text)
140
  shutil.move(temp_file.name, LOCAL_iBtrace_PATH)
141
+ # Remove the basin='west_pacific' parameter here as well
142
+ ibtracs = tracks.TrackDataset(source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
143
  with open(CACHE_FILE, 'wb') as f:
144
  pickle.dump(ibtracs, f)
145
  return ibtracs
 
325
 
326
  return tracks_fig, wind_scatter, pressure_scatter, regression_fig, slopes_text
327
 
328
+ # Video animation function with fixed sidebar and wind radius visualization
329
  def categorize_typhoon_by_standard(wind_speed, standard):
330
  if standard == 'taiwan':
331
  wind_speed_ms = wind_speed * 0.514444
 
351
  return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
352
  return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
353
 
354
+ def generate_track_video(year, basin, typhoon, standard):
355
  if not typhoon:
356
  return None
357
 
 
376
  ax.add_feature(cfeature.BORDERS, linestyle=':', edgecolor='gray')
377
  ax.gridlines(draw_labels=True, linestyle='--', color='gray', alpha=0.5)
378
 
379
+ ax.set_title(f"{year} {storm.name} Tropical Cyclone Path")
380
 
381
  # Initialize the line and point
382
  line, = ax.plot([], [], 'b-', linewidth=2, transform=ccrs.PlateCarree())
383
  point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
384
  date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
385
 
386
+ # Initialize wind radius circles for 34kt, 50kt, and 64kt
387
+ radius_patches = []
388
+ for _ in range(3):
389
+ patch = plt.Circle((0, 0), 0, fill=False, linewidth=2, visible=False, transform=ccrs.PlateCarree())
390
+ radius_patches.append(ax.add_patch(patch))
391
+
392
  # Add sidebar on the right with adjusted positions
393
+ details_title = fig.text(0.7, 0.95, "Cyclone Details", fontsize=12, fontweight='bold', verticalalignment='top')
394
  details_text = fig.text(0.7, 0.85, '', fontsize=12, verticalalignment='top',
395
  bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
396
 
 
402
  fig.legend(handles=legend_elements, title="Color Legend", loc='center right',
403
  bbox_to_anchor=(0.95, 0.5), fontsize=10)
404
 
405
+ # Add wind radius legend
406
+ radius_legend = [
407
+ plt.Line2D([0], [0], color='blue', label='34kt Gale Force'),
408
+ plt.Line2D([0], [0], color='orange', label='50kt Storm Force'),
409
+ plt.Line2D([0], [0], color='red', label='64kt Hurricane Force')
410
+ ]
411
+ fig.legend(handles=radius_legend, title="Wind Radii", loc='lower right',
412
+ bbox_to_anchor=(0.95, 0.15), fontsize=9)
413
+
414
  def init():
415
  line.set_data([], [])
416
  point.set_data([], [])
417
  date_text.set_text('')
418
  details_text.set_text('')
419
+ for patch in radius_patches:
420
+ patch.set_center((0, 0))
421
+ patch.set_radius(0)
422
+ patch.set_visible(False)
423
+ return [line, point, date_text, details_text] + radius_patches
424
 
425
  def update(frame):
426
  line.set_data(storm.lon[:frame+1], storm.lat[:frame+1])
 
428
  point.set_data([storm.lon[frame]], [storm.lat[frame]])
429
  point.set_color(color)
430
  date_text.set_text(storm.time[frame].strftime('%Y-%m-%d %H:%M'))
431
+
432
+ # Update wind radius circles
433
+ radius_info = []
434
+
435
+ # Check for radius data from different agencies
436
+ wind_thresholds = [(34, 'blue'), (50, 'orange'), (64, 'red')]
437
+
438
+ for i, (wind_kt, circle_color) in enumerate(wind_thresholds):
439
+ # Check USA agency radius data (average of all quadrants)
440
+ radius_values = []
441
+
442
+ # Check USA agency data
443
+ for quadrant in ['ne', 'se', 'sw', 'nw']:
444
+ attr = f'usa_r{wind_kt}_{quadrant}'
445
+ if hasattr(storm, attr) and frame < len(getattr(storm, attr)) and not np.isnan(getattr(storm, attr)[frame]):
446
+ radius_values.append(getattr(storm, attr)[frame])
447
+
448
+ # If no USA data, check BOM data
449
+ if not radius_values:
450
+ for quadrant in ['ne', 'se', 'sw', 'nw']:
451
+ attr = f'bom_r{wind_kt}_{quadrant}'
452
+ if hasattr(storm, attr) and frame < len(getattr(storm, attr)) and not np.isnan(getattr(storm, attr)[frame]):
453
+ radius_values.append(getattr(storm, attr)[frame])
454
+
455
+ # If still no data, try Reunion data
456
+ if not radius_values:
457
+ for quadrant in ['ne', 'se', 'sw', 'nw']:
458
+ attr = f'reunion_r{wind_kt}_{quadrant}'
459
+ if hasattr(storm, attr) and frame < len(getattr(storm, attr)) and not np.isnan(getattr(storm, attr)[frame]):
460
+ radius_values.append(getattr(storm, attr)[frame])
461
+
462
+ if radius_values:
463
+ # Calculate average radius (nautical miles)
464
+ avg_radius = np.mean(radius_values)
465
+
466
+ # Convert from nautical miles to approximate degrees (1 nm ≈ 1/60 degree)
467
+ radius_deg = avg_radius / 60.0
468
+
469
+ radius_patches[i].set_center((storm.lon[frame], storm.lat[frame]))
470
+ radius_patches[i].set_radius(radius_deg)
471
+ radius_patches[i].set_edgecolor(circle_color)
472
+ radius_patches[i].set_visible(True)
473
+
474
+ radius_info.append(f"{wind_kt}kt radius: {avg_radius:.1f} nm")
475
+ else:
476
+ radius_patches[i].set_visible(False)
477
+ radius_info.append(f"{wind_kt}kt radius: 0 nm")
478
+
479
+ # Add radius information to details
480
+ radius_text = "\n".join(radius_info)
481
+
482
+ # Get pressure value if available, otherwise show 'N/A'
483
+ pressure_value = storm.mslp[frame] if hasattr(storm, 'mslp') and frame < len(storm.mslp) and not np.isnan(storm.mslp[frame]) else 'N/A'
484
+ pressure_text = f"Pressure: {pressure_value if pressure_value != 'N/A' else 'N/A'} mb"
485
+
486
  details = f"Name: {storm.name}\n" \
487
  f"Date: {storm.time[frame].strftime('%Y-%m-%d %H:%M')}\n" \
488
  f"Wind Speed: {storm.vmax[frame]:.1f} kt\n" \
489
+ f"{pressure_text}\n" \
490
+ f"Category: {category}\n" \
491
+ f"\nWind Radii:\n{radius_text}"
492
+
493
  details_text.set_text(details)
494
+ return [line, point, date_text, details_text] + radius_patches
495
 
496
  ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(storm.time),
497
  interval=200, blit=True, repeat=True)
 
912
  - **Wind Analysis**: Examine wind speed vs ONI relationships
913
  - **Pressure Analysis**: Analyze pressure vs ONI relationships
914
  - **Longitude Analysis**: Study typhoon generation longitude vs ONI
915
+ - **Path Animation**: Watch animated typhoon paths with wind radius visualization
916
  - **TSNE Cluster**: Perform t-SNE clustering on typhoon routes with mean routes and region analysis
917
 
918
  Select a tab above to begin your analysis.
 
1069
  with gr.Tab("Typhoon Path Animation"):
1070
  with gr.Row():
1071
  year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950, 2025)], value="2024")
1072
+ basin_dropdown = gr.Dropdown(label="Basin", choices=["NA", "EP", "WP", "NI", "SI", "SP", "SA", "All"], value="WP")
1073
+ typhoon_dropdown = gr.Dropdown(label="Tropical Cyclone")
1074
  standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
1075
 
1076
  animate_btn = gr.Button("Generate Animation")
1077
+ path_video = gr.Video(label="Tropical Cyclone Path Animation", elem_id="path_video")
1078
  animation_info = gr.Markdown("""
1079
  ### Animation Instructions
1080
+ 1. Select a year and basin from the dropdowns
1081
+ 2. Choose a tropical cyclone from the populated list
1082
+ 3. Select a classification standard (Atlantic or Taiwan)
1083
+ 4. Click "Generate Animation"
1084
+ 5. The animation shows:
1085
+ - Tropical cyclone track growing over time with colored markers based on intensity
1086
+ - Wind radius circles (if data available) for 34kt (blue), 50kt (orange), and 64kt (red)
1087
+ - Date/time on the bottom left
1088
+ - Details sidebar showing name, date, wind speed, pressure, category, and wind radii
1089
+ - Color legend for storm categories and wind radii
1090
+
1091
+ Note: Wind radius data may not be available for all storms or all observation times.
1092
+ Different agencies use different wind speed averaging periods: USA (1-min), JTWC (1-min), JMA (10-min), IMD (3-min).
1093
  """)
1094
 
1095
+ def update_typhoon_options(year, basin):
1096
  season = ibtracs.get_season(int(year))
1097
  storm_summary = season.summary()
1098
+ if basin != "All":
1099
+ storm_summary = storm_summary[storm_summary['basin'] == basin]
1100
+ options = [f"{name} ({id})" for name, id in zip(storm_summary['name'], storm_summary['id'])]
1101
  return gr.update(choices=options, value=options[0] if options else None)
1102
 
1103
+ def update_basin_options(year):
1104
+ return gr.update(value="WP")
1105
+
1106
+ year_dropdown.change(fn=update_basin_options, inputs=year_dropdown, outputs=basin_dropdown)
1107
+ basin_dropdown.change(fn=update_typhoon_options, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
1108
+ year_dropdown.change(fn=update_typhoon_options, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
1109
+
1110
  animate_btn.click(
1111
  fn=generate_track_video,
1112
+ inputs=[year_dropdown, basin_dropdown, typhoon_dropdown, standard_dropdown],
1113
  outputs=path_video
1114
  )
1115