euler314 commited on
Commit
dd7cf2d
·
verified ·
1 Parent(s): 9d9f4d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +191 -0
app.py CHANGED
@@ -1142,7 +1142,198 @@ def calculate_track_uncertainty(track_points):
1142
  'intensity_max': np.max(intensity_uncertainty),
1143
  'track_length': len(track_points)
1144
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1146
  def create_genesis_animation(prediction_data, enable_animation=True):
1147
  """
1148
  Create professional typhoon track animation showing daily genesis potential and storm development
 
1142
  'intensity_max': np.max(intensity_uncertainty),
1143
  'track_length': len(track_points)
1144
  }
1145
+ def create_predict_animation(prediction_data, enable_animation=True):
1146
+ """
1147
+ Typhoon genesis PREDICT tab animation:
1148
+ shows monthly genesis-potential + progressive storm positions
1149
+ """
1150
+ try:
1151
+ daily_maps = prediction_data.get('daily_gpi_maps', [])
1152
+ if not daily_maps:
1153
+ return create_error_plot("No GPI data for prediction")
1154
+
1155
+ storms = prediction_data.get('storm_predictions', [])
1156
+ month = prediction_data['month']
1157
+ oni = prediction_data['oni_value']
1158
+ year = prediction_data.get('year', 2025)
1159
+
1160
+ # -- 1) static underlay: full storm routes (dashed gray lines)
1161
+ static_routes = []
1162
+ for s in storms:
1163
+ track = s.get('track', [])
1164
+ if not track: continue
1165
+ lats = [pt['lat'] for pt in track]
1166
+ lons = [pt['lon'] for pt in track]
1167
+ static_routes.append(
1168
+ go.Scattergeo(
1169
+ lat=lats, lon=lons,
1170
+ mode='lines',
1171
+ line=dict(width=2, dash='dash', color='gray'),
1172
+ showlegend=False, hoverinfo='skip'
1173
+ )
1174
+ )
1175
+
1176
+ # figure out map bounds
1177
+ all_lats = [pt['lat'] for s in storms for pt in s.get('track',[])]
1178
+ all_lons = [pt['lon'] for s in storms for pt in s.get('track',[])]
1179
+ mb = {
1180
+ 'lat_min': min(5, min(all_lats)-5) if all_lats else 5,
1181
+ 'lat_max': max(35, max(all_lats)+5) if all_lats else 35,
1182
+ 'lon_min': min(110, min(all_lons)-10) if all_lons else 110,
1183
+ 'lon_max': max(180, max(all_lons)+10) if all_lons else 180
1184
+ }
1185
+
1186
+ # -- 2) build frames
1187
+ frames = []
1188
+ for idx, day_data in enumerate(daily_maps):
1189
+ day = day_data['day']
1190
+ gpi = day_data['gpi_field']
1191
+ lats = day_data['lat_range']
1192
+ lons = day_data['lon_range']
1193
+
1194
+ traces = []
1195
+ # genesis‐potential scatter
1196
+ traces.append(go.Scattergeo(
1197
+ lat=np.repeat(lats, len(lons)),
1198
+ lon=np.tile(lons, len(lats)),
1199
+ mode='markers',
1200
+ marker=dict(
1201
+ size=6, color=gpi.flatten(),
1202
+ colorscale='Viridis', cmin=0, cmax=3, opacity=0.6,
1203
+ showscale=(idx==0),
1204
+ colorbar=(dict(
1205
+ title=dict(text="Genesis<br>Potential<br>Index", side="right")
1206
+ ) if idx==0 else None)
1207
+ ),
1208
+ name='GPI',
1209
+ showlegend=(idx==0),
1210
+ hovertemplate=(
1211
+ 'GPI: %{marker.color:.2f}<br>'
1212
+ 'Lat: %{lat:.1f}°N<br>'
1213
+ 'Lon: %{lon:.1f}°E<br>'
1214
+ f'Day {day} of {month:02d}/{year}<extra></extra>'
1215
+ )
1216
+ ))
1217
+
1218
+ # storm positions up to this day
1219
+ for s in storms:
1220
+ past = [pt for pt in s.get('track',[]) if pt['day'] <= day]
1221
+ if not past: continue
1222
+ lats_p = [pt['lat'] for pt in past]
1223
+ lons_p = [pt['lon'] for pt in past]
1224
+ intens = [pt['intensity'] for pt in past]
1225
+ cats = [pt['category'] for pt in past]
1226
+
1227
+ # line history
1228
+ traces.append(go.Scattergeo(
1229
+ lat=lats_p, lon=lons_p, mode='lines',
1230
+ line=dict(width=2, color='gray'),
1231
+ showlegend=(idx==0), hoverinfo='skip'
1232
+ ))
1233
+ # current position
1234
+ traces.append(go.Scattergeo(
1235
+ lat=[lats_p[-1]], lon=[lons_p[-1]],
1236
+ mode='markers',
1237
+ marker=dict(size=10, symbol='circle', color='red'),
1238
+ showlegend=(idx==0),
1239
+ hovertemplate=(
1240
+ f"{s['storm_id']}<br>"
1241
+ f"Intensity: {intens[-1]} kt<br>"
1242
+ f"Category: {cats[-1]}<extra></extra>"
1243
+ )
1244
+ ))
1245
+
1246
+ frames.append(go.Frame(
1247
+ data=traces,
1248
+ name=str(day), # ← name is REQUIRED as string :contentReference[oaicite:1]{index=1}
1249
+ layout=go.Layout(
1250
+ geo=dict(
1251
+ projection_type="natural earth",
1252
+ showland=True, landcolor="lightgray",
1253
+ showocean=True, oceancolor="lightblue",
1254
+ showcoastlines=True, coastlinecolor="darkgray",
1255
+ center=dict(lat=(mb['lat_min']+mb['lat_max'])/2,
1256
+ lon=(mb['lon_min']+mb['lon_max'])/2),
1257
+ lonaxis_range=[mb['lon_min'], mb['lon_max']],
1258
+ lataxis_range=[mb['lat_min'], mb['lat_max']],
1259
+ resolution=50
1260
+ ),
1261
+ title=f"Day {day} of {month:02d}/{year} | ONI: {oni:.2f}"
1262
+ )
1263
+ ))
1264
+
1265
+ # -- 3) initial Figure (static + first frame)
1266
+ init_data = static_routes + list(frames[0].data)
1267
+ fig = go.Figure(data=init_data, frames=frames)
1268
+
1269
+ # -- 4) play/pause + slider (redraw=True!)
1270
+ if enable_animation and len(frames)>1:
1271
+ steps = [
1272
+ dict(method="animate",
1273
+ args=[[fr.name],
1274
+ {"mode":"immediate",
1275
+ "frame":{"duration":600,"redraw":True},
1276
+ "transition":{"duration":0}}],
1277
+ label=fr.name)
1278
+ for fr in frames
1279
+ ]
1280
 
1281
+ fig.update_layout(
1282
+ updatemenus=[dict(
1283
+ type="buttons", showactive=False,
1284
+ x=1.05, y=0.05, xanchor="right", yanchor="bottom",
1285
+ buttons=[
1286
+ dict(label="▶ Play",
1287
+ method="animate",
1288
+ args=[None, # None=all frames
1289
+ {"frame":{"duration":600,"redraw":True}, # ← redraw fixes dead ▶
1290
+ "fromcurrent":True,"transition":{"duration":0}}]),
1291
+ dict(label="⏸ Pause",
1292
+ method="animate",
1293
+ args=[[None],
1294
+ {"frame":{"duration":0,"redraw":False},
1295
+ "mode":"immediate"}])
1296
+ ]
1297
+ )],
1298
+ sliders=[dict(active=0, pad=dict(t=50), steps=steps)]
1299
+ )
1300
+ else:
1301
+ # fallback: show only final day + static routes
1302
+ final = static_routes + list(frames[-1].data)
1303
+ fig = go.Figure(data=final)
1304
+
1305
+ # -- 5) shared layout styling
1306
+ fig.update_layout(
1307
+ title={
1308
+ 'text': f"🌊 Typhoon Prediction — {month:02d}/{year} | ONI: {oni:.2f}",
1309
+ 'x':0.5,'font':{'size':18}
1310
+ },
1311
+ geo=dict(
1312
+ projection_type="natural earth",
1313
+ showland=True, landcolor="lightgray",
1314
+ showocean=True, oceancolor="lightblue",
1315
+ showcoastlines=True, coastlinecolor="darkgray",
1316
+ showlakes=True, lakecolor="lightblue",
1317
+ showcountries=True, countrycolor="gray",
1318
+ resolution=50,
1319
+ center=dict(lat=20, lon=140),
1320
+ lonaxis_range=[110,180], lataxis_range=[5,35]
1321
+ ),
1322
+ width=1100, height=750,
1323
+ showlegend=True,
1324
+ legend=dict(
1325
+ x=0.02,y=0.98,
1326
+ bgcolor="rgba(255,255,255,0.7)",
1327
+ bordercolor="gray",borderwidth=1
1328
+ )
1329
+ )
1330
+
1331
+ return fig
1332
+
1333
+ except Exception as e:
1334
+ logging.error(f"Error in predict animation: {e}")
1335
+ import traceback; traceback.print_exc()
1336
+ return create_error_plot(f"Animation error: {e}")
1337
  def create_genesis_animation(prediction_data, enable_animation=True):
1338
  """
1339
  Create professional typhoon track animation showing daily genesis potential and storm development