Spaces:
Running
Running
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 |
|