Spaces:
Running
Running
Update app.py
Browse files
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 |
-
|
|
|
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 |
-
|
|
|
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}
|
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, "
|
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 |
-
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
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
|
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 |
-
|
|
|
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="
|
998 |
animation_info = gr.Markdown("""
|
999 |
### Animation Instructions
|
1000 |
-
1. Select a year and
|
1001 |
-
2. Choose a
|
1002 |
-
3.
|
1003 |
-
4.
|
1004 |
-
5. The animation shows
|
1005 |
-
-
|
1006 |
-
-
|
1007 |
-
-
|
|
|
|
|
|
|
|
|
|
|
1008 |
""")
|
1009 |
|
1010 |
-
def update_typhoon_options(year):
|
1011 |
season = ibtracs.get_season(int(year))
|
1012 |
storm_summary = season.summary()
|
1013 |
-
|
|
|
|
|
1014 |
return gr.update(choices=options, value=options[0] if options else None)
|
1015 |
|
1016 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|