Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -140,6 +140,14 @@ matplotlib_color_map = {
|
|
| 140 |
'C5 Super Typhoon': '#FF0000' # Red
|
| 141 |
}
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
def rgb_string_to_hex(rgb_string):
|
| 144 |
"""Convert 'rgb(r,g,b)' string to hex color for matplotlib"""
|
| 145 |
try:
|
|
@@ -158,6 +166,10 @@ def get_matplotlib_color(category):
|
|
| 158 |
"""Get matplotlib-compatible color for a storm category"""
|
| 159 |
return matplotlib_color_map.get(category, '#808080')
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
# Cluster colors for route visualization
|
| 162 |
CLUSTER_COLORS = [
|
| 163 |
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
|
@@ -724,6 +736,24 @@ def categorize_typhoon_enhanced(wind_speed):
|
|
| 724 |
else: # 137+ knots = Category 5 Super Typhoon
|
| 725 |
return 'C5 Super Typhoon'
|
| 726 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
# Original function for backward compatibility
|
| 728 |
def categorize_typhoon(wind_speed):
|
| 729 |
"""Original categorize typhoon function for backward compatibility"""
|
|
@@ -1007,8 +1037,8 @@ def cluster_storms(embedding, method='dbscan', eps=0.5, min_samples=3):
|
|
| 1007 |
# Return single cluster as fallback
|
| 1008 |
return np.array([0] * len(embedding))
|
| 1009 |
|
| 1010 |
-
def
|
| 1011 |
-
"""Create
|
| 1012 |
try:
|
| 1013 |
# Validate inputs
|
| 1014 |
if storm_features is None or storm_features.empty:
|
|
@@ -1043,115 +1073,71 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1043 |
storm_features_viz['NAME'] = 'UNNAMED'
|
| 1044 |
storm_features_viz['SEASON'] = 2000
|
| 1045 |
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
for i, cluster in enumerate(unique_clusters):
|
| 1061 |
-
cluster_data = storm_features_viz[storm_features_viz['cluster'] == cluster]
|
| 1062 |
-
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)] if cluster != -1 else '#CCCCCC'
|
| 1063 |
-
cluster_name = f'Cluster {cluster}' if cluster != -1 else 'Noise'
|
| 1064 |
-
|
| 1065 |
-
# FIXED: Add safe access to clustering features
|
| 1066 |
try:
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
min_pres = pd.Series([1000] * len(cluster_data))
|
| 1074 |
-
track_len = pd.Series([0] * len(cluster_data))
|
| 1075 |
-
|
| 1076 |
-
fig.add_trace(
|
| 1077 |
-
go.Scatter(
|
| 1078 |
-
x=cluster_data['dim1'],
|
| 1079 |
-
y=cluster_data['dim2'],
|
| 1080 |
-
mode='markers',
|
| 1081 |
-
marker=dict(color=color, size=8),
|
| 1082 |
-
name=cluster_name,
|
| 1083 |
-
hovertemplate=(
|
| 1084 |
-
'<b>%{customdata[0]}</b><br>'
|
| 1085 |
-
'Season: %{customdata[1]}<br>'
|
| 1086 |
-
'Max Wind: %{customdata[2]:.0f} kt<br>'
|
| 1087 |
-
'Min Pressure: %{customdata[3]:.0f} hPa<br>'
|
| 1088 |
-
'Track Length: %{customdata[4]:.0f} points<br>'
|
| 1089 |
-
'<extra></extra>'
|
| 1090 |
-
),
|
| 1091 |
-
customdata=np.column_stack((
|
| 1092 |
-
cluster_data['NAME'].fillna('UNNAMED'),
|
| 1093 |
-
cluster_data['SEASON'].fillna(2000),
|
| 1094 |
-
max_wind,
|
| 1095 |
-
min_pres,
|
| 1096 |
-
track_len
|
| 1097 |
-
))
|
| 1098 |
-
),
|
| 1099 |
-
row=1, col=1
|
| 1100 |
-
)
|
| 1101 |
-
|
| 1102 |
-
# Add route map - FIXED with better error handling
|
| 1103 |
-
for i, cluster in enumerate(unique_clusters):
|
| 1104 |
-
if cluster == -1: # Skip noise for route visualization
|
| 1105 |
-
continue
|
| 1106 |
-
|
| 1107 |
-
cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
|
| 1108 |
-
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
|
| 1109 |
-
|
| 1110 |
-
tracks_added = 0
|
| 1111 |
-
for j, sid in enumerate(cluster_storm_ids[:5]): # Limit to 5 storms per cluster for performance
|
| 1112 |
-
try:
|
| 1113 |
-
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 1114 |
if len(storm_track) > 1:
|
| 1115 |
-
|
| 1116 |
-
valid_coords = storm_track['LAT'].notna() & storm_track['LON'].notna()
|
| 1117 |
-
storm_track = storm_track[valid_coords]
|
| 1118 |
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
f'Cluster: {cluster}<br>'
|
| 1136 |
-
'<extra></extra>'
|
| 1137 |
-
)
|
| 1138 |
-
),
|
| 1139 |
-
row=1, col=2
|
| 1140 |
)
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
)
|
| 1152 |
-
|
| 1153 |
-
# Update geo layout
|
| 1154 |
-
fig.update_geos(
|
| 1155 |
projection_type="natural earth",
|
| 1156 |
showland=True,
|
| 1157 |
landcolor="LightGray",
|
|
@@ -1159,29 +1145,97 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1159 |
oceancolor="LightBlue",
|
| 1160 |
showcoastlines=True,
|
| 1161 |
coastlinecolor="Gray",
|
| 1162 |
-
center=dict(lat=20, lon=140)
|
| 1163 |
-
row=1, col=2
|
| 1164 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
|
| 1166 |
-
#
|
| 1167 |
-
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1185 |
|
| 1186 |
# Generate detailed cluster statistics - FIXED
|
| 1187 |
try:
|
|
@@ -1201,11 +1255,6 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1201 |
existing_cols = {k: v for k, v in available_cols.items() if v in storm_features_viz.columns}
|
| 1202 |
|
| 1203 |
if len(existing_cols) > 1: # Need at least SID + one other column
|
| 1204 |
-
cluster_stats = storm_features_viz.groupby('cluster').agg(
|
| 1205 |
-
{col: ['mean', 'std', 'count'] if col != 'SID' else 'count'
|
| 1206 |
-
for col in existing_cols.values()}
|
| 1207 |
-
).round(2)
|
| 1208 |
-
|
| 1209 |
stats_text = "ADVANCED CLUSTER ANALYSIS RESULTS\n" + "="*50 + "\n\n"
|
| 1210 |
|
| 1211 |
for cluster in sorted(storm_features_viz['cluster'].unique()):
|
|
@@ -1256,7 +1305,7 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1256 |
logging.error(f"Error generating cluster statistics: {stats_error}")
|
| 1257 |
stats_text = f"Error generating cluster statistics: {str(stats_error)}"
|
| 1258 |
|
| 1259 |
-
return
|
| 1260 |
|
| 1261 |
except Exception as e:
|
| 1262 |
logging.error(f"Error in clustering analysis: {e}")
|
|
@@ -1270,7 +1319,7 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1270 |
x=0.5, y=0.5, xanchor='center', yanchor='middle',
|
| 1271 |
showarrow=False, font_size=16
|
| 1272 |
)
|
| 1273 |
-
return error_fig, f"Error in clustering: {str(e)}"
|
| 1274 |
|
| 1275 |
# -----------------------------
|
| 1276 |
# ENHANCED: Advanced Prediction System with Route Forecasting
|
|
@@ -1513,18 +1562,18 @@ def predict_storm_route_and_intensity(lat, lon, month, oni_value, models=None, f
|
|
| 1513 |
}
|
| 1514 |
|
| 1515 |
def create_route_visualization(prediction_results, show_uncertainty=True):
|
| 1516 |
-
"""Create comprehensive route and intensity visualization"""
|
| 1517 |
try:
|
| 1518 |
if 'route_forecast' not in prediction_results or not prediction_results['route_forecast']:
|
| 1519 |
return None, "No route forecast data available"
|
| 1520 |
|
| 1521 |
route_data = prediction_results['route_forecast']
|
| 1522 |
|
| 1523 |
-
# Create subplot with route map and intensity evolution
|
| 1524 |
fig = make_subplots(
|
| 1525 |
rows=1, cols=2,
|
| 1526 |
subplot_titles=('Forecast Track', 'Intensity Evolution'),
|
| 1527 |
-
specs=[[{"type": "geo"}, {"type": "
|
| 1528 |
column_widths=[0.6, 0.4]
|
| 1529 |
)
|
| 1530 |
|
|
@@ -1619,7 +1668,7 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
|
|
| 1619 |
row=1, col=1
|
| 1620 |
)
|
| 1621 |
|
| 1622 |
-
# Intensity evolution plot
|
| 1623 |
fig.add_trace(
|
| 1624 |
go.Scatter(
|
| 1625 |
x=hours,
|
|
@@ -1658,7 +1707,7 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
|
|
| 1658 |
height=600
|
| 1659 |
)
|
| 1660 |
|
| 1661 |
-
# Update geo layout
|
| 1662 |
fig.update_geos(
|
| 1663 |
projection_type="natural earth",
|
| 1664 |
showland=True,
|
|
@@ -1672,7 +1721,7 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
|
|
| 1672 |
row=1, col=1
|
| 1673 |
)
|
| 1674 |
|
| 1675 |
-
# Update intensity plot
|
| 1676 |
fig.update_xaxes(title_text="Forecast Hour", row=1, col=2)
|
| 1677 |
fig.update_yaxes(title_text="Intensity (kt)", row=1, col=2)
|
| 1678 |
|
|
@@ -1922,7 +1971,12 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
| 1922 |
return 'Tropical Depression', '#808080'
|
| 1923 |
|
| 1924 |
if standard=='taiwan':
|
| 1925 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1926 |
if wind_speed_ms >= 51.0:
|
| 1927 |
return 'Strong Typhoon', '#FF0000' # Red
|
| 1928 |
elif wind_speed_ms >= 33.7:
|
|
@@ -1931,6 +1985,7 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
| 1931 |
return 'Mild Typhoon', '#FFFF00' # Yellow
|
| 1932 |
return 'Tropical Depression', '#808080' # Gray
|
| 1933 |
else:
|
|
|
|
| 1934 |
if wind_speed >= 137:
|
| 1935 |
return 'C5 Super Typhoon', '#FF0000' # Red
|
| 1936 |
elif wind_speed >= 113:
|
|
@@ -1946,7 +2001,7 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
| 1946 |
return 'Tropical Depression', '#808080' # Gray
|
| 1947 |
|
| 1948 |
# -----------------------------
|
| 1949 |
-
# ENHANCED: Animation Functions
|
| 1950 |
# -----------------------------
|
| 1951 |
|
| 1952 |
def get_available_years(typhoon_data):
|
|
@@ -2032,7 +2087,7 @@ def update_typhoon_options_enhanced(year, basin):
|
|
| 2032 |
return gr.update(choices=["Error loading storms"], value=None)
|
| 2033 |
|
| 2034 |
def generate_enhanced_track_video(year, typhoon_selection, standard):
|
| 2035 |
-
"""Enhanced track video generation with TD support and 2025 compatibility
|
| 2036 |
if not typhoon_selection or typhoon_selection == "No storms found":
|
| 2037 |
return None
|
| 2038 |
|
|
@@ -2063,10 +2118,10 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
|
|
| 2063 |
storm_name = storm_df['NAME'].iloc[0] if pd.notna(storm_df['NAME'].iloc[0]) else "UNNAMED"
|
| 2064 |
season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year
|
| 2065 |
|
| 2066 |
-
print(f"Generating video for {storm_name} ({sid}) with {len(lats)} track points")
|
| 2067 |
|
| 2068 |
# Create figure with enhanced map
|
| 2069 |
-
fig, ax = plt.subplots(figsize=(
|
| 2070 |
|
| 2071 |
# Enhanced map features
|
| 2072 |
ax.stock_img()
|
|
@@ -2086,28 +2141,37 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
|
|
| 2086 |
gl = ax.gridlines(draw_labels=True, alpha=0.3)
|
| 2087 |
gl.top_labels = gl.right_labels = False
|
| 2088 |
|
| 2089 |
-
# Title with enhanced info
|
| 2090 |
-
ax.set_title(f"{season} {storm_name} ({sid}) Track Animation
|
|
|
|
| 2091 |
|
| 2092 |
# Animation elements
|
| 2093 |
line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, label='Track')
|
| 2094 |
-
point, = ax.plot([], [], 'o', markersize=
|
| 2095 |
|
| 2096 |
# Enhanced info display
|
| 2097 |
info_box = ax.text(0.02, 0.98, '', transform=ax.transAxes,
|
| 2098 |
-
fontsize=
|
| 2099 |
bbox=dict(boxstyle="round,pad=0.5", facecolor='white', alpha=0.9))
|
| 2100 |
|
| 2101 |
-
# Color legend with
|
| 2102 |
legend_elements = []
|
| 2103 |
-
|
| 2104 |
-
|
| 2105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2106 |
color = get_matplotlib_color(category)
|
| 2107 |
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
|
| 2108 |
-
markerfacecolor=color, markersize=
|
| 2109 |
|
| 2110 |
-
ax.legend(handles=legend_elements, loc='upper right', fontsize=
|
| 2111 |
|
| 2112 |
def animate(frame):
|
| 2113 |
try:
|
|
@@ -2117,32 +2181,43 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
|
|
| 2117 |
# Update track line
|
| 2118 |
line.set_data(lons[:frame+1], lats[:frame+1])
|
| 2119 |
|
| 2120 |
-
# Update current position
|
| 2121 |
current_wind = winds[frame]
|
| 2122 |
-
|
| 2123 |
-
|
|
|
|
|
|
|
|
|
|
| 2124 |
|
| 2125 |
# Debug print for first few frames
|
| 2126 |
if frame < 3:
|
| 2127 |
-
print(f"Frame {frame}: Wind={current_wind:.1f}kt, Category={category}, Color={color}")
|
| 2128 |
|
| 2129 |
point.set_data([lons[frame]], [lats[frame]])
|
| 2130 |
point.set_color(color)
|
| 2131 |
-
point.set_markersize(
|
| 2132 |
|
| 2133 |
-
# Enhanced info display
|
| 2134 |
if 'ISO_TIME' in storm_df.columns and frame < len(storm_df):
|
| 2135 |
current_time = storm_df.iloc[frame]['ISO_TIME']
|
| 2136 |
time_str = current_time.strftime('%Y-%m-%d %H:%M UTC') if pd.notna(current_time) else 'Unknown'
|
| 2137 |
else:
|
| 2138 |
time_str = f"Step {frame+1}"
|
| 2139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2140 |
info_text = (
|
| 2141 |
f"Storm: {storm_name}\n"
|
| 2142 |
f"Time: {time_str}\n"
|
| 2143 |
f"Position: {lats[frame]:.1f}Β°N, {lons[frame]:.1f}Β°E\n"
|
| 2144 |
-
f"Max Wind: {
|
| 2145 |
f"Category: {category}\n"
|
|
|
|
| 2146 |
f"Frame: {frame+1}/{len(lats)}"
|
| 2147 |
)
|
| 2148 |
info_box.set_text(info_text)
|
|
@@ -2156,7 +2231,7 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
|
|
| 2156 |
# Create animation
|
| 2157 |
anim = animation.FuncAnimation(
|
| 2158 |
fig, animate, frames=len(lats),
|
| 2159 |
-
interval=
|
| 2160 |
)
|
| 2161 |
|
| 2162 |
# Save animation
|
|
@@ -2165,12 +2240,12 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
|
|
| 2165 |
|
| 2166 |
# Enhanced writer settings
|
| 2167 |
writer = animation.FFMpegWriter(
|
| 2168 |
-
fps=
|
| 2169 |
extra_args=['-pix_fmt', 'yuv420p'] # Better compatibility
|
| 2170 |
)
|
| 2171 |
|
| 2172 |
print(f"Saving animation to {temp_file.name}")
|
| 2173 |
-
anim.save(temp_file.name, writer=writer, dpi=
|
| 2174 |
plt.close(fig)
|
| 2175 |
|
| 2176 |
print(f"Video generated successfully: {temp_file.name}")
|
|
@@ -2236,7 +2311,7 @@ def initialize_data():
|
|
| 2236 |
initialize_data()
|
| 2237 |
|
| 2238 |
# -----------------------------
|
| 2239 |
-
# ENHANCED: Gradio Interface with
|
| 2240 |
# -----------------------------
|
| 2241 |
|
| 2242 |
def create_interface():
|
|
@@ -2271,11 +2346,12 @@ def create_interface():
|
|
| 2271 |
This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities.
|
| 2272 |
|
| 2273 |
### π Enhanced Features:
|
| 2274 |
-
- **Advanced ML Clustering**: UMAP/t-SNE storm pattern analysis with
|
| 2275 |
- **Predictive Routing**: Advanced storm track and intensity forecasting with uncertainty quantification
|
| 2276 |
- **Complete TD Support**: Now includes Tropical Depressions (< 34 kt)
|
|
|
|
| 2277 |
- **2025 Data Ready**: Real-time compatibility with current year data
|
| 2278 |
-
- **Enhanced Animations**: High-quality storm track visualizations
|
| 2279 |
|
| 2280 |
### π Data Status:
|
| 2281 |
- **ONI Data**: {len(oni_data)} years loaded
|
|
@@ -2299,8 +2375,8 @@ def create_interface():
|
|
| 2299 |
gr.Markdown(overview_text)
|
| 2300 |
|
| 2301 |
with gr.Tab("π¬ Advanced ML Clustering"):
|
| 2302 |
-
gr.Markdown("## π― Storm Pattern Analysis
|
| 2303 |
-
gr.Markdown("**
|
| 2304 |
|
| 2305 |
with gr.Row():
|
| 2306 |
with gr.Column(scale=2):
|
|
@@ -2311,52 +2387,61 @@ def create_interface():
|
|
| 2311 |
info="UMAP provides better global structure preservation"
|
| 2312 |
)
|
| 2313 |
with gr.Column(scale=1):
|
| 2314 |
-
|
| 2315 |
-
label="πΊοΈ Show Storm Routes on Map",
|
| 2316 |
-
value=True,
|
| 2317 |
-
info="Display actual storm tracks colored by cluster"
|
| 2318 |
-
)
|
| 2319 |
|
| 2320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2321 |
|
| 2322 |
with gr.Row():
|
| 2323 |
-
|
|
|
|
|
|
|
|
|
|
| 2324 |
|
| 2325 |
with gr.Row():
|
| 2326 |
cluster_stats = gr.Textbox(label="π Detailed Cluster Statistics", lines=15, max_lines=20)
|
| 2327 |
|
| 2328 |
-
def
|
| 2329 |
try:
|
| 2330 |
# Extract features for clustering
|
| 2331 |
storm_features = extract_storm_features(typhoon_data)
|
| 2332 |
if storm_features is None:
|
| 2333 |
-
return None, "Error: Could not extract storm features"
|
| 2334 |
-
|
| 2335 |
-
|
|
|
|
|
|
|
|
|
|
| 2336 |
except Exception as e:
|
| 2337 |
import traceback
|
| 2338 |
error_details = traceback.format_exc()
|
| 2339 |
-
|
|
|
|
| 2340 |
|
| 2341 |
analyze_clusters_btn.click(
|
| 2342 |
-
fn=
|
| 2343 |
-
inputs=[reduction_method
|
| 2344 |
-
outputs=[cluster_plot, cluster_stats]
|
| 2345 |
)
|
| 2346 |
|
| 2347 |
cluster_info_text = """
|
| 2348 |
-
### π
|
|
|
|
| 2349 |
- **Multi-dimensional Analysis**: Uses 15+ storm characteristics including intensity, track shape, genesis location
|
| 2350 |
-
- **Route Visualization**:
|
| 2351 |
-
- **
|
| 2352 |
-
- **
|
| 2353 |
-
- **Interactive**: Hover over points to see storm details, zoom and pan
|
| 2354 |
|
| 2355 |
### π― How to Interpret:
|
| 2356 |
-
- **
|
| 2357 |
-
- **
|
| 2358 |
-
- **
|
| 2359 |
-
- **
|
|
|
|
| 2360 |
"""
|
| 2361 |
gr.Markdown(cluster_info_text)
|
| 2362 |
|
|
@@ -2521,7 +2606,7 @@ def create_interface():
|
|
| 2521 |
)
|
| 2522 |
|
| 2523 |
with gr.Tab("π¬ Enhanced Track Animation"):
|
| 2524 |
-
gr.Markdown("## π₯ High-Quality Storm Track Visualization (
|
| 2525 |
|
| 2526 |
with gr.Row():
|
| 2527 |
year_dropdown = gr.Dropdown(
|
|
@@ -2538,9 +2623,10 @@ def create_interface():
|
|
| 2538 |
with gr.Row():
|
| 2539 |
typhoon_dropdown = gr.Dropdown(label="Storm Selection (All Categories Including TD)")
|
| 2540 |
standard_dropdown = gr.Dropdown(
|
| 2541 |
-
label="Classification Standard",
|
| 2542 |
choices=['atlantic', 'taiwan'],
|
| 2543 |
-
value='atlantic'
|
|
|
|
| 2544 |
)
|
| 2545 |
|
| 2546 |
generate_video_btn = gr.Button("π¬ Generate Enhanced Animation", variant="primary")
|
|
@@ -2563,6 +2649,7 @@ def create_interface():
|
|
| 2563 |
|
| 2564 |
animation_info_text = """
|
| 2565 |
### π¬ Enhanced Animation Features:
|
|
|
|
| 2566 |
- **Full TD Support**: Now displays Tropical Depressions (< 34 kt) in gray
|
| 2567 |
- **2025 Compatibility**: Complete support for current year data
|
| 2568 |
- **Enhanced Maps**: Better cartographic projections with terrain features
|
|
@@ -2570,6 +2657,12 @@ def create_interface():
|
|
| 2570 |
- **Real-time Info**: Live position, time, and meteorological data display
|
| 2571 |
- **Professional Styling**: Publication-quality animations with proper legends
|
| 2572 |
- **Optimized Export**: Fast rendering with web-compatible video formats
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2573 |
"""
|
| 2574 |
gr.Markdown(animation_info_text)
|
| 2575 |
|
|
@@ -2694,7 +2787,8 @@ def create_interface():
|
|
| 2694 |
|
| 2695 |
### π Platform Capabilities:
|
| 2696 |
- **Complete TD Analysis** - First platform to include comprehensive TD tracking
|
| 2697 |
-
- **
|
|
|
|
| 2698 |
- **Real-time Predictions** - Physics-based and optional CNN intensity forecasting
|
| 2699 |
- **2025 Data Ready** - Full compatibility with current season data
|
| 2700 |
- **Enhanced Animations** - Professional-quality storm track videos
|
|
@@ -2706,6 +2800,7 @@ def create_interface():
|
|
| 2706 |
- Storm pattern classification
|
| 2707 |
- ENSO-typhoon relationship analysis
|
| 2708 |
- Intensity prediction model development
|
|
|
|
| 2709 |
"""
|
| 2710 |
gr.Markdown(stats_text)
|
| 2711 |
|
|
|
|
| 140 |
'C5 Super Typhoon': '#FF0000' # Red
|
| 141 |
}
|
| 142 |
|
| 143 |
+
# Taiwan color mapping
|
| 144 |
+
taiwan_color_map = {
|
| 145 |
+
'Tropical Depression': '#808080', # Gray
|
| 146 |
+
'Mild Typhoon': '#FFFF00', # Yellow
|
| 147 |
+
'Medium Typhoon': '#FFA500', # Orange
|
| 148 |
+
'Strong Typhoon': '#FF0000' # Red
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
def rgb_string_to_hex(rgb_string):
|
| 152 |
"""Convert 'rgb(r,g,b)' string to hex color for matplotlib"""
|
| 153 |
try:
|
|
|
|
| 166 |
"""Get matplotlib-compatible color for a storm category"""
|
| 167 |
return matplotlib_color_map.get(category, '#808080')
|
| 168 |
|
| 169 |
+
def get_taiwan_color(category):
|
| 170 |
+
"""Get Taiwan standard color for a storm category"""
|
| 171 |
+
return taiwan_color_map.get(category, '#808080')
|
| 172 |
+
|
| 173 |
# Cluster colors for route visualization
|
| 174 |
CLUSTER_COLORS = [
|
| 175 |
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
|
|
|
| 736 |
else: # 137+ knots = Category 5 Super Typhoon
|
| 737 |
return 'C5 Super Typhoon'
|
| 738 |
|
| 739 |
+
def categorize_typhoon_taiwan(wind_speed):
|
| 740 |
+
"""Taiwan categorization system"""
|
| 741 |
+
if pd.isna(wind_speed):
|
| 742 |
+
return 'Tropical Depression'
|
| 743 |
+
|
| 744 |
+
# Convert to m/s if in knots
|
| 745 |
+
if wind_speed > 50: # Likely in knots, convert to m/s
|
| 746 |
+
wind_speed = wind_speed * 0.514444
|
| 747 |
+
|
| 748 |
+
if wind_speed >= 51.0:
|
| 749 |
+
return 'Strong Typhoon'
|
| 750 |
+
elif wind_speed >= 33.7:
|
| 751 |
+
return 'Medium Typhoon'
|
| 752 |
+
elif wind_speed >= 17.2:
|
| 753 |
+
return 'Mild Typhoon'
|
| 754 |
+
else:
|
| 755 |
+
return 'Tropical Depression'
|
| 756 |
+
|
| 757 |
# Original function for backward compatibility
|
| 758 |
def categorize_typhoon(wind_speed):
|
| 759 |
"""Original categorize typhoon function for backward compatibility"""
|
|
|
|
| 1037 |
# Return single cluster as fallback
|
| 1038 |
return np.array([0] * len(embedding))
|
| 1039 |
|
| 1040 |
+
def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'):
|
| 1041 |
+
"""Create separate plots for clustering analysis - FIXED VERSION"""
|
| 1042 |
try:
|
| 1043 |
# Validate inputs
|
| 1044 |
if storm_features is None or storm_features.empty:
|
|
|
|
| 1073 |
storm_features_viz['NAME'] = 'UNNAMED'
|
| 1074 |
storm_features_viz['SEASON'] = 2000
|
| 1075 |
|
| 1076 |
+
# 1. Clustering scatter plot
|
| 1077 |
+
fig_cluster = px.scatter(
|
| 1078 |
+
storm_features_viz,
|
| 1079 |
+
x='dim1',
|
| 1080 |
+
y='dim2',
|
| 1081 |
+
color='cluster',
|
| 1082 |
+
hover_data=['NAME', 'SEASON'],
|
| 1083 |
+
title=f'Storm Clustering using {method.upper()}',
|
| 1084 |
+
labels={
|
| 1085 |
+
'dim1': f'{method.upper()} Dimension 1',
|
| 1086 |
+
'dim2': f'{method.upper()} Dimension 2',
|
| 1087 |
+
'cluster': 'Cluster'
|
| 1088 |
+
}
|
| 1089 |
+
)
|
| 1090 |
+
|
| 1091 |
+
# 2. Route map
|
| 1092 |
+
fig_routes = go.Figure()
|
| 1093 |
+
|
| 1094 |
+
unique_clusters = sorted(storm_features_viz['cluster'].unique())
|
| 1095 |
+
for i, cluster in enumerate(unique_clusters):
|
| 1096 |
+
if cluster == -1: # Skip noise for route visualization
|
| 1097 |
+
continue
|
| 1098 |
+
|
| 1099 |
+
cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
|
| 1100 |
+
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
|
| 1101 |
|
| 1102 |
+
tracks_added = 0
|
| 1103 |
+
for j, sid in enumerate(cluster_storm_ids[:5]): # Limit to 5 storms per cluster for performance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1104 |
try:
|
| 1105 |
+
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 1106 |
+
if len(storm_track) > 1:
|
| 1107 |
+
# Ensure valid coordinates
|
| 1108 |
+
valid_coords = storm_track['LAT'].notna() & storm_track['LON'].notna()
|
| 1109 |
+
storm_track = storm_track[valid_coords]
|
| 1110 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
if len(storm_track) > 1:
|
| 1112 |
+
storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
|
|
|
|
|
|
|
| 1113 |
|
| 1114 |
+
fig_routes.add_trace(
|
| 1115 |
+
go.Scattergeo(
|
| 1116 |
+
lon=storm_track['LON'],
|
| 1117 |
+
lat=storm_track['LAT'],
|
| 1118 |
+
mode='lines+markers',
|
| 1119 |
+
line=dict(color=color, width=2),
|
| 1120 |
+
marker=dict(color=color, size=4),
|
| 1121 |
+
name=f'C{cluster}: {storm_name}' if tracks_added == 0 else None,
|
| 1122 |
+
showlegend=(tracks_added == 0),
|
| 1123 |
+
hovertemplate=(
|
| 1124 |
+
f'<b>{storm_name}</b><br>'
|
| 1125 |
+
'Lat: %{lat:.1f}Β°<br>'
|
| 1126 |
+
'Lon: %{lon:.1f}Β°<br>'
|
| 1127 |
+
f'Cluster: {cluster}<br>'
|
| 1128 |
+
'<extra></extra>'
|
| 1129 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
)
|
| 1131 |
+
)
|
| 1132 |
+
tracks_added += 1
|
| 1133 |
+
except Exception as track_error:
|
| 1134 |
+
logging.warning(f"Error adding track for storm {sid}: {track_error}")
|
| 1135 |
+
continue
|
| 1136 |
+
|
| 1137 |
+
# Update route map layout
|
| 1138 |
+
fig_routes.update_layout(
|
| 1139 |
+
title="Clustered Storm Routes",
|
| 1140 |
+
geo=dict(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
projection_type="natural earth",
|
| 1142 |
showland=True,
|
| 1143 |
landcolor="LightGray",
|
|
|
|
| 1145 |
oceancolor="LightBlue",
|
| 1146 |
showcoastlines=True,
|
| 1147 |
coastlinecolor="Gray",
|
| 1148 |
+
center=dict(lat=20, lon=140)
|
|
|
|
| 1149 |
)
|
| 1150 |
+
)
|
| 1151 |
+
|
| 1152 |
+
# 3. Pressure evolution plot
|
| 1153 |
+
fig_pressure = go.Figure()
|
| 1154 |
+
|
| 1155 |
+
for i, cluster in enumerate(unique_clusters):
|
| 1156 |
+
if cluster == -1:
|
| 1157 |
+
continue
|
| 1158 |
+
|
| 1159 |
+
cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
|
| 1160 |
+
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
|
| 1161 |
|
| 1162 |
+
for j, sid in enumerate(cluster_storm_ids[:3]): # Limit to 3 storms per cluster
|
| 1163 |
+
try:
|
| 1164 |
+
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 1165 |
+
if len(storm_track) > 1 and 'USA_PRES' in storm_track.columns:
|
| 1166 |
+
pressure_values = pd.to_numeric(storm_track['USA_PRES'], errors='coerce').dropna()
|
| 1167 |
+
if len(pressure_values) > 0:
|
| 1168 |
+
storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
|
| 1169 |
+
time_hours = range(len(pressure_values))
|
| 1170 |
+
|
| 1171 |
+
fig_pressure.add_trace(
|
| 1172 |
+
go.Scatter(
|
| 1173 |
+
x=time_hours,
|
| 1174 |
+
y=pressure_values,
|
| 1175 |
+
mode='lines',
|
| 1176 |
+
line=dict(color=color, width=2),
|
| 1177 |
+
name=f'C{cluster}: {storm_name}' if j == 0 else None,
|
| 1178 |
+
showlegend=(j == 0),
|
| 1179 |
+
hovertemplate=(
|
| 1180 |
+
f'<b>{storm_name}</b><br>'
|
| 1181 |
+
'Hour: %{x}<br>'
|
| 1182 |
+
'Pressure: %{y:.0f} hPa<br>'
|
| 1183 |
+
'<extra></extra>'
|
| 1184 |
+
)
|
| 1185 |
+
)
|
| 1186 |
+
)
|
| 1187 |
+
except Exception as e:
|
| 1188 |
+
continue
|
| 1189 |
+
|
| 1190 |
+
fig_pressure.update_layout(
|
| 1191 |
+
title="Pressure Evolution by Cluster",
|
| 1192 |
+
xaxis_title="Time (hours)",
|
| 1193 |
+
yaxis_title="Pressure (hPa)"
|
| 1194 |
+
)
|
| 1195 |
+
|
| 1196 |
+
# 4. Wind evolution plot
|
| 1197 |
+
fig_wind = go.Figure()
|
| 1198 |
+
|
| 1199 |
+
for i, cluster in enumerate(unique_clusters):
|
| 1200 |
+
if cluster == -1:
|
| 1201 |
+
continue
|
| 1202 |
+
|
| 1203 |
+
cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
|
| 1204 |
+
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
|
| 1205 |
|
| 1206 |
+
for j, sid in enumerate(cluster_storm_ids[:3]): # Limit to 3 storms per cluster
|
| 1207 |
+
try:
|
| 1208 |
+
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 1209 |
+
if len(storm_track) > 1 and 'USA_WIND' in storm_track.columns:
|
| 1210 |
+
wind_values = pd.to_numeric(storm_track['USA_WIND'], errors='coerce').dropna()
|
| 1211 |
+
if len(wind_values) > 0:
|
| 1212 |
+
storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
|
| 1213 |
+
time_hours = range(len(wind_values))
|
| 1214 |
+
|
| 1215 |
+
fig_wind.add_trace(
|
| 1216 |
+
go.Scatter(
|
| 1217 |
+
x=time_hours,
|
| 1218 |
+
y=wind_values,
|
| 1219 |
+
mode='lines',
|
| 1220 |
+
line=dict(color=color, width=2),
|
| 1221 |
+
name=f'C{cluster}: {storm_name}' if j == 0 else None,
|
| 1222 |
+
showlegend=(j == 0),
|
| 1223 |
+
hovertemplate=(
|
| 1224 |
+
f'<b>{storm_name}</b><br>'
|
| 1225 |
+
'Hour: %{x}<br>'
|
| 1226 |
+
'Wind: %{y:.0f} kt<br>'
|
| 1227 |
+
'<extra></extra>'
|
| 1228 |
+
)
|
| 1229 |
+
)
|
| 1230 |
+
)
|
| 1231 |
+
except Exception as e:
|
| 1232 |
+
continue
|
| 1233 |
+
|
| 1234 |
+
fig_wind.update_layout(
|
| 1235 |
+
title="Wind Speed Evolution by Cluster",
|
| 1236 |
+
xaxis_title="Time (hours)",
|
| 1237 |
+
yaxis_title="Wind Speed (kt)"
|
| 1238 |
+
)
|
| 1239 |
|
| 1240 |
# Generate detailed cluster statistics - FIXED
|
| 1241 |
try:
|
|
|
|
| 1255 |
existing_cols = {k: v for k, v in available_cols.items() if v in storm_features_viz.columns}
|
| 1256 |
|
| 1257 |
if len(existing_cols) > 1: # Need at least SID + one other column
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1258 |
stats_text = "ADVANCED CLUSTER ANALYSIS RESULTS\n" + "="*50 + "\n\n"
|
| 1259 |
|
| 1260 |
for cluster in sorted(storm_features_viz['cluster'].unique()):
|
|
|
|
| 1305 |
logging.error(f"Error generating cluster statistics: {stats_error}")
|
| 1306 |
stats_text = f"Error generating cluster statistics: {str(stats_error)}"
|
| 1307 |
|
| 1308 |
+
return fig_cluster, fig_routes, fig_pressure, fig_wind, stats_text
|
| 1309 |
|
| 1310 |
except Exception as e:
|
| 1311 |
logging.error(f"Error in clustering analysis: {e}")
|
|
|
|
| 1319 |
x=0.5, y=0.5, xanchor='center', yanchor='middle',
|
| 1320 |
showarrow=False, font_size=16
|
| 1321 |
)
|
| 1322 |
+
return error_fig, error_fig, error_fig, error_fig, f"Error in clustering: {str(e)}"
|
| 1323 |
|
| 1324 |
# -----------------------------
|
| 1325 |
# ENHANCED: Advanced Prediction System with Route Forecasting
|
|
|
|
| 1562 |
}
|
| 1563 |
|
| 1564 |
def create_route_visualization(prediction_results, show_uncertainty=True):
|
| 1565 |
+
"""Create comprehensive route and intensity visualization - FIXED"""
|
| 1566 |
try:
|
| 1567 |
if 'route_forecast' not in prediction_results or not prediction_results['route_forecast']:
|
| 1568 |
return None, "No route forecast data available"
|
| 1569 |
|
| 1570 |
route_data = prediction_results['route_forecast']
|
| 1571 |
|
| 1572 |
+
# Create subplot with route map and intensity evolution - FIXED
|
| 1573 |
fig = make_subplots(
|
| 1574 |
rows=1, cols=2,
|
| 1575 |
subplot_titles=('Forecast Track', 'Intensity Evolution'),
|
| 1576 |
+
specs=[[{"type": "geo"}, {"type": "xy"}]], # Changed to xy for regular plot
|
| 1577 |
column_widths=[0.6, 0.4]
|
| 1578 |
)
|
| 1579 |
|
|
|
|
| 1668 |
row=1, col=1
|
| 1669 |
)
|
| 1670 |
|
| 1671 |
+
# Intensity evolution plot - FIXED to use regular scatter plot
|
| 1672 |
fig.add_trace(
|
| 1673 |
go.Scatter(
|
| 1674 |
x=hours,
|
|
|
|
| 1707 |
height=600
|
| 1708 |
)
|
| 1709 |
|
| 1710 |
+
# Update geo layout - FIXED: Only update geo subplot
|
| 1711 |
fig.update_geos(
|
| 1712 |
projection_type="natural earth",
|
| 1713 |
showland=True,
|
|
|
|
| 1721 |
row=1, col=1
|
| 1722 |
)
|
| 1723 |
|
| 1724 |
+
# Update intensity plot - FIXED: Use correct method for regular subplot
|
| 1725 |
fig.update_xaxes(title_text="Forecast Hour", row=1, col=2)
|
| 1726 |
fig.update_yaxes(title_text="Intensity (kt)", row=1, col=2)
|
| 1727 |
|
|
|
|
| 1971 |
return 'Tropical Depression', '#808080'
|
| 1972 |
|
| 1973 |
if standard=='taiwan':
|
| 1974 |
+
# Taiwan standard uses m/s, convert if needed
|
| 1975 |
+
if wind_speed > 50: # Likely in knots, convert to m/s
|
| 1976 |
+
wind_speed_ms = wind_speed * 0.514444
|
| 1977 |
+
else:
|
| 1978 |
+
wind_speed_ms = wind_speed
|
| 1979 |
+
|
| 1980 |
if wind_speed_ms >= 51.0:
|
| 1981 |
return 'Strong Typhoon', '#FF0000' # Red
|
| 1982 |
elif wind_speed_ms >= 33.7:
|
|
|
|
| 1985 |
return 'Mild Typhoon', '#FFFF00' # Yellow
|
| 1986 |
return 'Tropical Depression', '#808080' # Gray
|
| 1987 |
else:
|
| 1988 |
+
# Atlantic standard in knots
|
| 1989 |
if wind_speed >= 137:
|
| 1990 |
return 'C5 Super Typhoon', '#FF0000' # Red
|
| 1991 |
elif wind_speed >= 113:
|
|
|
|
| 2001 |
return 'Tropical Depression', '#808080' # Gray
|
| 2002 |
|
| 2003 |
# -----------------------------
|
| 2004 |
+
# ENHANCED: Animation Functions with Taiwan Standard Support
|
| 2005 |
# -----------------------------
|
| 2006 |
|
| 2007 |
def get_available_years(typhoon_data):
|
|
|
|
| 2087 |
return gr.update(choices=["Error loading storms"], value=None)
|
| 2088 |
|
| 2089 |
def generate_enhanced_track_video(year, typhoon_selection, standard):
|
| 2090 |
+
"""Enhanced track video generation with TD support, Taiwan standard, and 2025 compatibility"""
|
| 2091 |
if not typhoon_selection or typhoon_selection == "No storms found":
|
| 2092 |
return None
|
| 2093 |
|
|
|
|
| 2118 |
storm_name = storm_df['NAME'].iloc[0] if pd.notna(storm_df['NAME'].iloc[0]) else "UNNAMED"
|
| 2119 |
season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year
|
| 2120 |
|
| 2121 |
+
print(f"Generating video for {storm_name} ({sid}) with {len(lats)} track points using {standard} standard")
|
| 2122 |
|
| 2123 |
# Create figure with enhanced map
|
| 2124 |
+
fig, ax = plt.subplots(figsize=(16, 10), subplot_kw={'projection': ccrs.PlateCarree()})
|
| 2125 |
|
| 2126 |
# Enhanced map features
|
| 2127 |
ax.stock_img()
|
|
|
|
| 2141 |
gl = ax.gridlines(draw_labels=True, alpha=0.3)
|
| 2142 |
gl.top_labels = gl.right_labels = False
|
| 2143 |
|
| 2144 |
+
# Title with enhanced info and standard
|
| 2145 |
+
ax.set_title(f"{season} {storm_name} ({sid}) Track Animation - {standard.upper()} Standard",
|
| 2146 |
+
fontsize=18, fontweight='bold')
|
| 2147 |
|
| 2148 |
# Animation elements
|
| 2149 |
line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, label='Track')
|
| 2150 |
+
point, = ax.plot([], [], 'o', markersize=15)
|
| 2151 |
|
| 2152 |
# Enhanced info display
|
| 2153 |
info_box = ax.text(0.02, 0.98, '', transform=ax.transAxes,
|
| 2154 |
+
fontsize=12, verticalalignment='top',
|
| 2155 |
bbox=dict(boxstyle="round,pad=0.5", facecolor='white', alpha=0.9))
|
| 2156 |
|
| 2157 |
+
# Color legend with both standards - ENHANCED
|
| 2158 |
legend_elements = []
|
| 2159 |
+
|
| 2160 |
+
if standard == 'taiwan':
|
| 2161 |
+
categories = ['Tropical Depression', 'Mild Typhoon', 'Medium Typhoon', 'Strong Typhoon']
|
| 2162 |
+
for category in categories:
|
| 2163 |
+
color = get_taiwan_color(category)
|
| 2164 |
+
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
|
| 2165 |
+
markerfacecolor=color, markersize=10, label=category))
|
| 2166 |
+
else:
|
| 2167 |
+
categories = ['Tropical Depression', 'Tropical Storm', 'C1 Typhoon', 'C2 Typhoon',
|
| 2168 |
+
'C3 Strong Typhoon', 'C4 Very Strong Typhoon', 'C5 Super Typhoon']
|
| 2169 |
+
for category in categories:
|
| 2170 |
color = get_matplotlib_color(category)
|
| 2171 |
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
|
| 2172 |
+
markerfacecolor=color, markersize=10, label=category))
|
| 2173 |
|
| 2174 |
+
ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
|
| 2175 |
|
| 2176 |
def animate(frame):
|
| 2177 |
try:
|
|
|
|
| 2181 |
# Update track line
|
| 2182 |
line.set_data(lons[:frame+1], lats[:frame+1])
|
| 2183 |
|
| 2184 |
+
# Update current position with appropriate categorization
|
| 2185 |
current_wind = winds[frame]
|
| 2186 |
+
|
| 2187 |
+
if standard == 'taiwan':
|
| 2188 |
+
category, color = categorize_typhoon_by_standard(current_wind, 'taiwan')
|
| 2189 |
+
else:
|
| 2190 |
+
category, color = categorize_typhoon_by_standard(current_wind, 'atlantic')
|
| 2191 |
|
| 2192 |
# Debug print for first few frames
|
| 2193 |
if frame < 3:
|
| 2194 |
+
print(f"Frame {frame}: Wind={current_wind:.1f}kt, Category={category}, Color={color}, Standard={standard}")
|
| 2195 |
|
| 2196 |
point.set_data([lons[frame]], [lats[frame]])
|
| 2197 |
point.set_color(color)
|
| 2198 |
+
point.set_markersize(10 + current_wind/8) # Size based on intensity
|
| 2199 |
|
| 2200 |
+
# Enhanced info display with standard information
|
| 2201 |
if 'ISO_TIME' in storm_df.columns and frame < len(storm_df):
|
| 2202 |
current_time = storm_df.iloc[frame]['ISO_TIME']
|
| 2203 |
time_str = current_time.strftime('%Y-%m-%d %H:%M UTC') if pd.notna(current_time) else 'Unknown'
|
| 2204 |
else:
|
| 2205 |
time_str = f"Step {frame+1}"
|
| 2206 |
|
| 2207 |
+
# Convert wind speed for Taiwan standard display
|
| 2208 |
+
if standard == 'taiwan':
|
| 2209 |
+
wind_ms = current_wind * 0.514444 # Convert to m/s for display
|
| 2210 |
+
wind_display = f"{current_wind:.0f} kt ({wind_ms:.1f} m/s)"
|
| 2211 |
+
else:
|
| 2212 |
+
wind_display = f"{current_wind:.0f} kt"
|
| 2213 |
+
|
| 2214 |
info_text = (
|
| 2215 |
f"Storm: {storm_name}\n"
|
| 2216 |
f"Time: {time_str}\n"
|
| 2217 |
f"Position: {lats[frame]:.1f}Β°N, {lons[frame]:.1f}Β°E\n"
|
| 2218 |
+
f"Max Wind: {wind_display}\n"
|
| 2219 |
f"Category: {category}\n"
|
| 2220 |
+
f"Standard: {standard.upper()}\n"
|
| 2221 |
f"Frame: {frame+1}/{len(lats)}"
|
| 2222 |
)
|
| 2223 |
info_box.set_text(info_text)
|
|
|
|
| 2231 |
# Create animation
|
| 2232 |
anim = animation.FuncAnimation(
|
| 2233 |
fig, animate, frames=len(lats),
|
| 2234 |
+
interval=400, blit=False, repeat=True # Slightly slower for better viewing
|
| 2235 |
)
|
| 2236 |
|
| 2237 |
# Save animation
|
|
|
|
| 2240 |
|
| 2241 |
# Enhanced writer settings
|
| 2242 |
writer = animation.FFMpegWriter(
|
| 2243 |
+
fps=3, bitrate=2000, codec='libx264', # Slower FPS for better visibility
|
| 2244 |
extra_args=['-pix_fmt', 'yuv420p'] # Better compatibility
|
| 2245 |
)
|
| 2246 |
|
| 2247 |
print(f"Saving animation to {temp_file.name}")
|
| 2248 |
+
anim.save(temp_file.name, writer=writer, dpi=120) # Higher DPI for better quality
|
| 2249 |
plt.close(fig)
|
| 2250 |
|
| 2251 |
print(f"Video generated successfully: {temp_file.name}")
|
|
|
|
| 2311 |
initialize_data()
|
| 2312 |
|
| 2313 |
# -----------------------------
|
| 2314 |
+
# ENHANCED: Gradio Interface with Fixed Route Visualization and Enhanced Features
|
| 2315 |
# -----------------------------
|
| 2316 |
|
| 2317 |
def create_interface():
|
|
|
|
| 2346 |
This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities.
|
| 2347 |
|
| 2348 |
### π Enhanced Features:
|
| 2349 |
+
- **Advanced ML Clustering**: UMAP/t-SNE storm pattern analysis with separate visualizations
|
| 2350 |
- **Predictive Routing**: Advanced storm track and intensity forecasting with uncertainty quantification
|
| 2351 |
- **Complete TD Support**: Now includes Tropical Depressions (< 34 kt)
|
| 2352 |
+
- **Taiwan Standard**: Full support for Taiwan meteorological classification system
|
| 2353 |
- **2025 Data Ready**: Real-time compatibility with current year data
|
| 2354 |
+
- **Enhanced Animations**: High-quality storm track visualizations with both standards
|
| 2355 |
|
| 2356 |
### π Data Status:
|
| 2357 |
- **ONI Data**: {len(oni_data)} years loaded
|
|
|
|
| 2375 |
gr.Markdown(overview_text)
|
| 2376 |
|
| 2377 |
with gr.Tab("π¬ Advanced ML Clustering"):
|
| 2378 |
+
gr.Markdown("## π― Storm Pattern Analysis with Separate Visualizations")
|
| 2379 |
+
gr.Markdown("**Four separate plots: Clustering, Routes, Pressure Evolution, and Wind Evolution**")
|
| 2380 |
|
| 2381 |
with gr.Row():
|
| 2382 |
with gr.Column(scale=2):
|
|
|
|
| 2387 |
info="UMAP provides better global structure preservation"
|
| 2388 |
)
|
| 2389 |
with gr.Column(scale=1):
|
| 2390 |
+
analyze_clusters_btn = gr.Button("π Generate All Cluster Analyses", variant="primary", size="lg")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2391 |
|
| 2392 |
+
with gr.Row():
|
| 2393 |
+
with gr.Column():
|
| 2394 |
+
cluster_plot = gr.Plot(label="π Storm Clustering Analysis")
|
| 2395 |
+
with gr.Column():
|
| 2396 |
+
routes_plot = gr.Plot(label="πΊοΈ Clustered Storm Routes")
|
| 2397 |
|
| 2398 |
with gr.Row():
|
| 2399 |
+
with gr.Column():
|
| 2400 |
+
pressure_plot = gr.Plot(label="π‘οΈ Pressure Evolution by Cluster")
|
| 2401 |
+
with gr.Column():
|
| 2402 |
+
wind_plot = gr.Plot(label="π¨ Wind Speed Evolution by Cluster")
|
| 2403 |
|
| 2404 |
with gr.Row():
|
| 2405 |
cluster_stats = gr.Textbox(label="π Detailed Cluster Statistics", lines=15, max_lines=20)
|
| 2406 |
|
| 2407 |
+
def run_separate_clustering_analysis(method):
|
| 2408 |
try:
|
| 2409 |
# Extract features for clustering
|
| 2410 |
storm_features = extract_storm_features(typhoon_data)
|
| 2411 |
if storm_features is None:
|
| 2412 |
+
return None, None, None, None, "Error: Could not extract storm features"
|
| 2413 |
+
|
| 2414 |
+
fig_cluster, fig_routes, fig_pressure, fig_wind, stats = create_separate_clustering_plots(
|
| 2415 |
+
storm_features, typhoon_data, method.lower()
|
| 2416 |
+
)
|
| 2417 |
+
return fig_cluster, fig_routes, fig_pressure, fig_wind, stats
|
| 2418 |
except Exception as e:
|
| 2419 |
import traceback
|
| 2420 |
error_details = traceback.format_exc()
|
| 2421 |
+
error_msg = f"Error: {str(e)}\n\nDetails:\n{error_details}"
|
| 2422 |
+
return None, None, None, None, error_msg
|
| 2423 |
|
| 2424 |
analyze_clusters_btn.click(
|
| 2425 |
+
fn=run_separate_clustering_analysis,
|
| 2426 |
+
inputs=[reduction_method],
|
| 2427 |
+
outputs=[cluster_plot, routes_plot, pressure_plot, wind_plot, cluster_stats]
|
| 2428 |
)
|
| 2429 |
|
| 2430 |
cluster_info_text = """
|
| 2431 |
+
### π Enhanced Clustering Features:
|
| 2432 |
+
- **Separate Visualizations**: Four distinct plots for comprehensive analysis
|
| 2433 |
- **Multi-dimensional Analysis**: Uses 15+ storm characteristics including intensity, track shape, genesis location
|
| 2434 |
+
- **Route Visualization**: Geographic storm tracks colored by cluster membership
|
| 2435 |
+
- **Temporal Analysis**: Pressure and wind evolution patterns by cluster
|
| 2436 |
+
- **DBSCAN Clustering**: Automatic pattern discovery without predefined cluster count
|
| 2437 |
+
- **Interactive**: Hover over points to see storm details, zoom and pan all plots
|
| 2438 |
|
| 2439 |
### π― How to Interpret:
|
| 2440 |
+
- **Clustering Plot**: Each dot is a storm positioned by similarity (close = similar characteristics)
|
| 2441 |
+
- **Routes Plot**: Actual geographic storm tracks, colored by which cluster they belong to
|
| 2442 |
+
- **Pressure Plot**: Shows how pressure changes over time for storms in each cluster
|
| 2443 |
+
- **Wind Plot**: Shows wind speed evolution patterns for each cluster
|
| 2444 |
+
- **Cluster Colors**: Each cluster gets a unique color across all four visualizations
|
| 2445 |
"""
|
| 2446 |
gr.Markdown(cluster_info_text)
|
| 2447 |
|
|
|
|
| 2606 |
)
|
| 2607 |
|
| 2608 |
with gr.Tab("π¬ Enhanced Track Animation"):
|
| 2609 |
+
gr.Markdown("## π₯ High-Quality Storm Track Visualization (Atlantic & Taiwan Standards)")
|
| 2610 |
|
| 2611 |
with gr.Row():
|
| 2612 |
year_dropdown = gr.Dropdown(
|
|
|
|
| 2623 |
with gr.Row():
|
| 2624 |
typhoon_dropdown = gr.Dropdown(label="Storm Selection (All Categories Including TD)")
|
| 2625 |
standard_dropdown = gr.Dropdown(
|
| 2626 |
+
label="π Classification Standard",
|
| 2627 |
choices=['atlantic', 'taiwan'],
|
| 2628 |
+
value='atlantic',
|
| 2629 |
+
info="Atlantic: International standard | Taiwan: Local meteorological standard"
|
| 2630 |
)
|
| 2631 |
|
| 2632 |
generate_video_btn = gr.Button("π¬ Generate Enhanced Animation", variant="primary")
|
|
|
|
| 2649 |
|
| 2650 |
animation_info_text = """
|
| 2651 |
### π¬ Enhanced Animation Features:
|
| 2652 |
+
- **Dual Standards**: Full support for both Atlantic and Taiwan classification systems
|
| 2653 |
- **Full TD Support**: Now displays Tropical Depressions (< 34 kt) in gray
|
| 2654 |
- **2025 Compatibility**: Complete support for current year data
|
| 2655 |
- **Enhanced Maps**: Better cartographic projections with terrain features
|
|
|
|
| 2657 |
- **Real-time Info**: Live position, time, and meteorological data display
|
| 2658 |
- **Professional Styling**: Publication-quality animations with proper legends
|
| 2659 |
- **Optimized Export**: Fast rendering with web-compatible video formats
|
| 2660 |
+
|
| 2661 |
+
### π Taiwan Standard Features:
|
| 2662 |
+
- **m/s Display**: Shows both knots and meters per second
|
| 2663 |
+
- **Local Categories**: TD β Mild β Medium β Strong Typhoon
|
| 2664 |
+
- **Color Coding**: Gray β Yellow β Orange β Red
|
| 2665 |
+
- **CWB Compatible**: Matches Central Weather Bureau classifications
|
| 2666 |
"""
|
| 2667 |
gr.Markdown(animation_info_text)
|
| 2668 |
|
|
|
|
| 2787 |
|
| 2788 |
### π Platform Capabilities:
|
| 2789 |
- **Complete TD Analysis** - First platform to include comprehensive TD tracking
|
| 2790 |
+
- **Dual Classification Systems** - Both Atlantic and Taiwan standards supported
|
| 2791 |
+
- **Advanced ML Clustering** - DBSCAN pattern recognition with separate visualizations
|
| 2792 |
- **Real-time Predictions** - Physics-based and optional CNN intensity forecasting
|
| 2793 |
- **2025 Data Ready** - Full compatibility with current season data
|
| 2794 |
- **Enhanced Animations** - Professional-quality storm track videos
|
|
|
|
| 2800 |
- Storm pattern classification
|
| 2801 |
- ENSO-typhoon relationship analysis
|
| 2802 |
- Intensity prediction model development
|
| 2803 |
+
- Cross-regional classification comparisons
|
| 2804 |
"""
|
| 2805 |
gr.Markdown(stats_text)
|
| 2806 |
|