euler314 commited on
Commit
c222226
·
verified ·
1 Parent(s): 1ad4a07

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -268
app.py CHANGED
@@ -1149,315 +1149,184 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1149
  Following NHC/JTWC visualization standards with proper geographic map and time controls
1150
  """
1151
  try:
1152
- if 'daily_gpi_maps' not in prediction_data or not prediction_data['daily_gpi_maps']:
 
1153
  return create_error_plot("No GPI data available for animation")
1154
 
1155
- daily_maps = prediction_data['daily_gpi_maps']
1156
  storm_predictions = prediction_data.get('storm_predictions', [])
1157
  month = prediction_data['month']
1158
  oni_value = prediction_data['oni_value']
1159
  year = prediction_data.get('year', 2025)
1160
 
1161
- # Create base figure with geographic map
1162
- fig = go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1163
 
1164
- if enable_animation and len(daily_maps) > 1:
1165
- # PROFESSIONAL ANIMATION VERSION
1166
- frames = []
1167
-
1168
- # Get all storm track data for proper bounds
1169
- all_lats, all_lons = [], []
1170
- for storm in storm_predictions:
1171
- track = storm.get('track', [])
1172
- if track:
1173
- all_lats.extend([pt['lat'] for pt in track])
1174
- all_lons.extend([pt['lon'] for pt in track])
1175
 
1176
- # Set map bounds (Western Pacific focus)
1177
- map_bounds = {
1178
- 'lat_min': min(5, min(all_lats) - 5) if all_lats else 5,
1179
- 'lat_max': max(35, max(all_lats) + 5) if all_lats else 35,
1180
- 'lon_min': min(110, min(all_lons) - 10) if all_lons else 110,
1181
- 'lon_max': max(180, max(all_lons) + 10) if all_lons else 180
1182
- }
1183
-
1184
- # Create frames for each day
1185
- for day_idx, day_data in enumerate(daily_maps):
1186
- day = day_data['day']
1187
- frame_data = []
1188
-
1189
- # 1. GENESIS POTENTIAL BACKGROUND (GPI Contours)
1190
- gpi_field = day_data['gpi_field']
1191
- lat_range = day_data['lat_range']
1192
- lon_range = day_data['lon_range']
1193
-
1194
- frame_data.append(
1195
- go.Scattergeo(
1196
- lat=np.repeat(lat_range, len(lon_range)),
1197
- lon=np.tile(lon_range, len(lat_range)),
1198
- mode='markers',
1199
- marker=dict(
1200
- size=6,
1201
- color=gpi_field.flatten(),
1202
- colorscale='Viridis',
1203
- cmin=0,
1204
- cmax=3,
1205
- opacity=0.6,
1206
- showscale=(day_idx == 0),
1207
- colorbar=dict(
1208
- title="Genesis<br>Potential<br>Index",
1209
- title_side="right" # ← fixed here
1210
- ) if day_idx == 0 else None
1211
- ),
1212
- name='Genesis Potential',
1213
- showlegend=(day_idx == 0),
1214
- hovertemplate=(
1215
- 'Genesis Potential<br>'
1216
- 'Lat: %{lat:.1f}°N<br>'
1217
- 'Lon: %{lon:.1f}°E<br>'
1218
- 'GPI: %{marker.color:.2f}<br>'
1219
- f'Day {day} of {month:02d}/{year}<br>'
1220
- '<extra></extra>'
1221
  )
1222
- )
 
 
 
 
 
 
 
 
1223
  )
 
 
 
 
 
 
 
 
 
 
 
1224
 
1225
- # 2. STORM TRACKS (Progressive reveal like NHC)
1226
- for storm_idx, storm in enumerate(storm_predictions):
1227
- track = storm['track']
1228
- storm_id = storm['storm_id']
1229
-
1230
- # Get track points up to current day
1231
- current_track = [pt for pt in track if pt['day'] <= day]
1232
-
1233
- if current_track and len(current_track) > 0:
1234
- lats = [pt['lat'] for pt in current_track]
1235
- lons = [pt['lon'] for pt in current_track]
1236
- intensities = [pt['intensity'] for pt in current_track]
1237
- categories = [pt['category'] for pt in current_track]
1238
-
1239
- # Get colors based on intensity (Saffir-Simpson-like scale)
1240
- colors = []
1241
- for intensity in intensities:
1242
- if intensity < 34:
1243
- colors.append('#80808080') # Gray - TD
1244
- elif intensity < 64:
1245
- colors.append('#0000FF80') # Blue - TS
1246
- elif intensity < 83:
1247
- colors.append('#00FF0080') # Green - Cat 1
1248
- elif intensity < 96:
1249
- colors.append('#FFFF0080') # Yellow - Cat 2
1250
- elif intensity < 113:
1251
- colors.append('#FFA50080') # Orange - Cat 3
1252
- elif intensity < 137:
1253
- colors.append('#FF000080') # Red - Cat 4
1254
- else:
1255
- colors.append('#8B000080') # DarkRed - Cat 5
1256
-
1257
- # Historical track line
1258
- frame_data.append(
1259
- go.Scattergeo(
1260
- lat=lats,
1261
- lon=lons,
1262
- mode='lines',
1263
- line=dict(
1264
- width=2,
1265
- color='gray'
1266
- ),
1267
- name=f"{storm_id} Track",
1268
- showlegend=(day_idx == 0),
1269
- hoverinfo='skip'
1270
- )
1271
- )
1272
-
1273
- # Current position marker
1274
- frame_data.append(
1275
- go.Scattergeo(
1276
- lat=[lats[-1]],
1277
- lon=[lons[-1]],
1278
- mode='markers',
1279
- marker=dict(
1280
- size=10,
1281
- symbol='circle',
1282
- color=colors[-1]
1283
- ),
1284
- name=f"{storm_id} Position",
1285
- showlegend=(day_idx == 0),
1286
- hovertemplate=(
1287
- f"{storm_id}<br>"
1288
- f"Lat: {lats[-1]:.1f}°N<br>"
1289
- f"Lon: {lons[-1]:.1f}°E<br>"
1290
- f"Intensity: {intensities[-1]} kt<br>"
1291
- f"Category: {categories[-1]}<br>"
1292
- '<extra></extra>'
1293
- )
1294
- )
1295
- )
1296
-
1297
- # Create frame
1298
- frames.append(go.Frame(
1299
- data=frame_data,
1300
- name=str(day),
1301
- layout=go.Layout(
1302
- title=f"Typhoon Development Forecast - Day {day}, {month:02d}/{year} | ONI: {oni_value:.2f}",
1303
- geo=dict(
1304
- projection_type="natural earth",
1305
- showland=True,
1306
- landcolor="lightgray",
1307
- showocean=True,
1308
- oceancolor="lightblue",
1309
- showcoastlines=True,
1310
- coastlinecolor="darkgray",
1311
- center=dict(
1312
- lat=(map_bounds['lat_min'] + map_bounds['lat_max']) / 2,
1313
- lon=(map_bounds['lon_min'] + map_bounds['lon_max']) / 2
1314
- ),
1315
- lonaxis_range=[map_bounds['lon_min'], map_bounds['lon_max']],
1316
- lataxis_range=[map_bounds['lat_min'], map_bounds['lat_max']],
1317
- resolution=50
1318
- )
1319
  )
1320
  ))
1321
 
1322
- fig.frames = frames
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1323
 
1324
- # Set up initial frame, add play/pause controls, sliders, etc.
1325
- slider_steps = [
1326
- dict(method="animate",
1327
- args=[[frame.name], {
1328
- "mode": "immediate",
1329
- "frame": {"duration": 500, "redraw": True},
1330
- "transition": {"duration": 0}
1331
- }],
1332
- label=frame.name)
1333
- for frame in frames
1334
- ]
1335
 
1336
  fig.update_layout(
1337
  updatemenus=[dict(
1338
- type="buttons",
1339
- showactive=False,
1340
- y=0,
1341
- x=1.05,
1342
- xanchor="right",
1343
- yanchor="top",
1344
- pad=dict(t=0, r=10),
1345
  buttons=[
1346
- dict(label="▶️ Play",
1347
- method="animate",
1348
- args=[None, {
1349
- "frame": {"duration": 500, "redraw": True},
1350
- "fromcurrent": True,
1351
- "transition": {"duration": 0}
1352
- }]),
1353
- dict(label="⏸️ Pause",
1354
- method="animate",
1355
- args=[[None], {
1356
- "frame": {"duration": 0, "redraw": False},
1357
- "mode": "immediate",
1358
- "transition": {"duration": 0}
1359
- }])
1360
  ]
1361
  )],
1362
  sliders=[dict(
1363
- active=0,
1364
- pad=dict(t=50),
1365
- steps=slider_steps
1366
  )]
1367
  )
1368
-
1369
  else:
1370
- # STATIC VERSION (Final state overview)
1371
- final_day = daily_maps[-1]
1372
- gpi_field = final_day['gpi_field']
1373
- lat_range = final_day['lat_range']
1374
- lon_range = final_day['lon_range']
1375
-
1376
- fig.add_trace(
1377
- go.Scattergeo(
1378
- lat=np.repeat(lat_range, len(lon_range)),
1379
- lon=np.tile(lon_range, len(lat_range)),
1380
- mode='markers',
1381
- marker=dict(
1382
- size=6,
1383
- color=gpi_field.flatten(),
1384
- colorscale='Viridis',
1385
- opacity=0.6,
1386
- showscale=True,
1387
- colorbar=dict(
1388
- title="Genesis Potential Index"
1389
- )
1390
- ),
1391
- name='Genesis Potential'
1392
- )
1393
- )
1394
-
1395
- # Add complete storm tracks
1396
- for storm in storm_predictions:
1397
- track = storm['track']
1398
- if track:
1399
- lats = [pt['lat'] for pt in track]
1400
- lons = [pt['lon'] for pt in track]
1401
- fig.add_trace(
1402
- go.Scattergeo(
1403
- lat=lats,
1404
- lon=lons,
1405
- mode='lines+markers',
1406
- line=dict(width=2, color='gray'),
1407
- marker=dict(
1408
- size=6,
1409
- symbol='circle',
1410
- color='red'
1411
- ),
1412
- name=f"{storm['storm_id']} Full Track"
1413
- )
1414
- )
1415
 
1416
- # FINAL LAYOUT (Professional meteorological style)
1417
  fig.update_layout(
1418
  title={
1419
- 'text': f"🌊 Typhoon Genesis & Development Forecast<br><sub>{month:02d}/{year} | ONI: {oni_value:.2f} | " +
1420
- ("Interactive Animation" if enable_animation else "Monthly Summary") + "</sub>",
1421
- 'x': 0.5,
1422
- 'font': {'size': 18}
1423
  },
1424
  geo=dict(
1425
  projection_type="natural earth",
1426
- showland=True,
1427
- landcolor="lightgray",
1428
- showocean=True,
1429
- oceancolor="lightblue",
1430
- showcoastlines=True,
1431
- coastlinecolor="darkgray",
1432
- showlakes=True,
1433
- lakecolor="lightblue",
1434
- showcountries=True,
1435
- countrycolor="gray",
1436
  resolution=50,
1437
  center=dict(lat=20, lon=140),
1438
- lonaxis_range=[110, 180],
1439
- lataxis_range=[5, 35]
1440
  ),
1441
- width=1200,
1442
- height=800,
1443
  showlegend=True,
1444
- legend=dict(
1445
- x=0.02,
1446
- y=0.98,
1447
- bgcolor="rgba(255,255,255,0.8)",
1448
- bordercolor="gray",
1449
- borderwidth=1
1450
- )
1451
  )
1452
 
1453
  return fig
1454
-
1455
  except Exception as e:
1456
  logging.error(f"Error creating professional genesis animation: {e}")
1457
- import traceback
1458
- traceback.print_exc()
1459
- return create_error_plot(f"Animation error: {str(e)}")
1460
-
1461
  def create_error_plot(error_message):
1462
  """Create a simple error plot"""
1463
  fig = go.Figure()
 
1149
  Following NHC/JTWC visualization standards with proper geographic map and time controls
1150
  """
1151
  try:
1152
+ daily_maps = prediction_data.get('daily_gpi_maps', [])
1153
+ if not daily_maps:
1154
  return create_error_plot("No GPI data available for animation")
1155
 
 
1156
  storm_predictions = prediction_data.get('storm_predictions', [])
1157
  month = prediction_data['month']
1158
  oni_value = prediction_data['oni_value']
1159
  year = prediction_data.get('year', 2025)
1160
 
1161
+ # ---- Build animation frames ----
1162
+ frames = []
1163
+
1164
+ # Determine map bounds from all storm tracks
1165
+ all_lats, all_lons = [], []
1166
+ for storm in storm_predictions:
1167
+ track = storm.get('track', [])
1168
+ for pt in track:
1169
+ all_lats.append(pt['lat'])
1170
+ all_lons.append(pt['lon'])
1171
+ map_bounds = {
1172
+ 'lat_min': min(5, min(all_lats) - 5) if all_lats else 5,
1173
+ 'lat_max': max(35, max(all_lats) + 5) if all_lats else 35,
1174
+ 'lon_min': min(110, min(all_lons) - 10) if all_lons else 110,
1175
+ 'lon_max': max(180, max(all_lons) + 10) if all_lons else 180
1176
+ }
1177
 
1178
+ for day_idx, day_data in enumerate(daily_maps):
1179
+ day = day_data['day']
1180
+ gpi = day_data['gpi_field']
1181
+ lats = day_data['lat_range']
1182
+ lons = day_data['lon_range']
 
 
 
 
 
 
1183
 
1184
+ traces = []
1185
+ # 1) Genesis Potential dots
1186
+ traces.append(go.Scattergeo(
1187
+ lat=np.repeat(lats, len(lons)),
1188
+ lon=np.tile(lons, len(lats)),
1189
+ mode='markers',
1190
+ marker=dict(
1191
+ size=6,
1192
+ color=gpi.flatten(),
1193
+ colorscale='Viridis',
1194
+ cmin=0, cmax=3, opacity=0.6,
1195
+ showscale=(day_idx == 0),
1196
+ colorbar=(dict(
1197
+ title=dict(
1198
+ text="Genesis<br>Potential<br>Index",
1199
+ side="right"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1200
  )
1201
+ ) if day_idx == 0 else None)
1202
+ ),
1203
+ name='Genesis Potential',
1204
+ showlegend=(day_idx == 0),
1205
+ hovertemplate=(
1206
+ 'GPI: %{marker.color:.2f}<br>' +
1207
+ 'Lat: %{lat:.1f}°N<br>' +
1208
+ 'Lon: %{lon:.1f}°E<br>' +
1209
+ f'Day {day} of {month:02d}/{year}<extra></extra>'
1210
  )
1211
+ ))
1212
+
1213
+ # 2) Storm tracks up to this day
1214
+ for storm in storm_predictions:
1215
+ track = [pt for pt in storm.get('track', []) if pt['day'] <= day]
1216
+ if not track:
1217
+ continue
1218
+ track_lats = [pt['lat'] for pt in track]
1219
+ track_lons = [pt['lon'] for pt in track]
1220
+ intensities = [pt['intensity'] for pt in track]
1221
+ categories = [pt['category'] for pt in track]
1222
 
1223
+ # a) historical line
1224
+ traces.append(go.Scattergeo(
1225
+ lat=track_lats, lon=track_lons, mode='lines',
1226
+ line=dict(width=2, color='gray'),
1227
+ name=f"{storm['storm_id']} Track",
1228
+ showlegend=(day_idx == 0),
1229
+ hoverinfo='skip'
1230
+ ))
1231
+ # b) current position
1232
+ traces.append(go.Scattergeo(
1233
+ lat=[track_lats[-1]], lon=[track_lons[-1]], mode='markers',
1234
+ marker=dict(size=10, symbol='circle', color='red'),
1235
+ name=f"{storm['storm_id']} Position",
1236
+ showlegend=(day_idx == 0),
1237
+ hovertemplate=(
1238
+ f"{storm['storm_id']}<br>"
1239
+ f"Intensity: {intensities[-1]} kt<br>"
1240
+ f"Category: {categories[-1]}<extra></extra>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1241
  )
1242
  ))
1243
 
1244
+ frames.append(go.Frame(
1245
+ data=traces,
1246
+ name=str(day),
1247
+ layout=go.Layout(
1248
+ geo=dict(
1249
+ projection_type="natural earth",
1250
+ showland=True, landcolor="lightgray",
1251
+ showocean=True, oceancolor="lightblue",
1252
+ showcoastlines=True, coastlinecolor="darkgray",
1253
+ center=dict(
1254
+ lat=(map_bounds['lat_min'] + map_bounds['lat_max'])/2,
1255
+ lon=(map_bounds['lon_min'] + map_bounds['lon_max'])/2
1256
+ ),
1257
+ lonaxis_range=[map_bounds['lon_min'], map_bounds['lon_max']],
1258
+ lataxis_range=[map_bounds['lat_min'], map_bounds['lat_max']],
1259
+ resolution=50
1260
+ ),
1261
+ title=f"Day {day} of {month:02d}/{year} ONI: {oni_value:.2f}"
1262
+ )
1263
+ ))
1264
+
1265
+ # ---- Initialize Figure with first frame’s data ----
1266
+ if enable_animation and len(frames) > 1:
1267
+ fig = go.Figure(
1268
+ data=frames[0].data,
1269
+ frames=frames
1270
+ )
1271
 
1272
+ # Build slider steps
1273
+ steps = []
1274
+ for fr in frames:
1275
+ steps.append(dict(
1276
+ method="animate",
1277
+ args=[[fr.name], {"mode":"immediate","frame":{"duration":600,"redraw":True},"transition":{"duration":0}}],
1278
+ label=fr.name
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", method="animate",
1287
+ args=[None, {"frame":{"duration":600,"redraw":True},"fromcurrent":True,"transition":{"duration":0}}]),
1288
+ dict(label="⏸ Pause", method="animate",
1289
+ args=[[None], {"frame":{"duration":0,"redraw":False},"mode":"immediate"}])
 
 
 
 
 
 
 
 
 
 
1290
  ]
1291
  )],
1292
  sliders=[dict(
1293
+ active=0, pad=dict(t=50),
1294
+ steps=steps
 
1295
  )]
1296
  )
 
1297
  else:
1298
+ # STATIC fallback: show only final day
1299
+ final = frames[-1].data
1300
+ fig = go.Figure(data=final)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1301
 
1302
+ # ---- Shared final layout styling ----
1303
  fig.update_layout(
1304
  title={
1305
+ 'text': f"🌊 Typhoon Genesis & Development Forecast<br><sub>{month:02d}/{year} | ONI: {oni_value:.2f}</sub>",
1306
+ 'x':0.5, 'font':{'size':18}
 
 
1307
  },
1308
  geo=dict(
1309
  projection_type="natural earth",
1310
+ showland=True, landcolor="lightgray",
1311
+ showocean=True, oceancolor="lightblue",
1312
+ showcoastlines=True, coastlinecolor="darkgray",
1313
+ showlakes=True, lakecolor="lightblue",
1314
+ showcountries=True, countrycolor="gray",
 
 
 
 
 
1315
  resolution=50,
1316
  center=dict(lat=20, lon=140),
1317
+ lonaxis_range=[110,180], lataxis_range=[5,35]
 
1318
  ),
1319
+ width=1100, height=750,
 
1320
  showlegend=True,
1321
+ legend=dict(x=0.02, y=0.98, bgcolor="rgba(255,255,255,0.7)", bordercolor="gray", borderwidth=1)
 
 
 
 
 
 
1322
  )
1323
 
1324
  return fig
1325
+
1326
  except Exception as e:
1327
  logging.error(f"Error creating professional genesis animation: {e}")
1328
+ import traceback; traceback.print_exc()
1329
+ return create_error_plot(f"Animation error: {e}")
 
 
1330
  def create_error_plot(error_message):
1331
  """Create a simple error plot"""
1332
  fig = go.Figure()